HTTP JSON API first version (#1994)

* Cleanup

* WIP

* first integration test + fixture

* minor cleanup

* Implementing ContractService.lookup

* Reverting back to endpoints.all (all2 did not work)

* Cleanup

* replace ApiValue ADT with aliases to daml-lf/transaction Value ADT

* porting rest of navigator to LF Value ADT

* Command Service WIP

* CommandService WIP

* porting more of navigator to LF Value ADT

* last error, not first

* rename ApiValueImplicits file

* special conversion features for ImmArray and FrontStack

- just .to[ImmArray] or .to[FrontStack] any random collection

* finish porting most of navigator main code

* use numeric indices for record field name fallback when pretty-printing

* tuples are not serializable

* use numeric indices for label fallback in JSON verbose encoding

* make traverseEitherStrictly more likely to preserve the seq's class

* to shortcut for ImmArraySeq .to[ImmArraySeq]

* compiling, passing navigator backend tests

* test traverseEitherStrictly more, er, strictly

* pass scalacopts through to scaladoc

* deal with unused warning

* remove unneeded function

* simpler error reporting, more private functions in ApiCodecCompressed

* move slowApply to FrontStack, test it so it actually works

* remove unneeded toStrings; better error from impossible ValueTuple case

* scalafmt FrontStackSpec

* support alternative, label-free record JSON encoding

* Adding domain.CreateCommand + corresponding json formats and dummy json format for lav1.value.Record

* CommandService.create should be done... need to test it

* TODO added

* Cleanup

* move ApiCodecCompressed, ApiValueImplicits, and some aliases to new lf-value-json package

* Using tagged TemplateId type instead of Identifier + exercise command WIP

* adapt navigator to moved pieces

* start defining scalacheck extension to ApiCodecCompressedSpec

* CommandService.exercise + introducing CommandMeta

* Adding command endpoints, can't test them yet, need lf value json formats

* fuse some list operations

- suggested by @stefanobaghino-da; thanks

* blue error message

* Minor fixes after merging librify-navigator-json-compressed, #2136

* experiment with an inductive case in TypedValueGenerators

* finish a List case for TypedValueGenerators; it's revealing

* Introducing API value to LF value converter,

CommandsValidator takes IdentifierResolverLike instead of IdentifierResolver

* cleanup

* remove accidentally readded duplicate aliases

* start tying knots in TypedValueGenerators

* verbatim copy ApiCodecCompressedSpec to lf-value-json

* shift some tests from navigator to lf-value-json

* test Optional and Map for ApiCodecCompressed

* heavier random testing of ApiCodecCompressed

* remove unused dependencies from lf-value-json

* adding value json writer

* cleanup

* Revert "cleanup"

This reverts commit 2e4d153f

* fixing the build

* cleanup

* cleaning up imports

* JsValue to API value is done, needs a test

* cleanup

* use scalac -Ypartial-unification in http-json

* simplify some Traverse instances

* factor CreateCommand and ExerciseCommand traverse instances

* Command create integration test WIP

* Command create integration test WIP, got rid of the JsonReader and JsonWriter for the values, converting values explicitly

* Extracting DomainJsonDecoder and DomainJsonEncoder

* LfV refactoring

* Create command serialize/deserialize test works

* cleanup

* resolving conflicts

* More json encode/decode tests

* logging

* command/create passes integration test now

* Adding readme

* grammar

* TODO added

* GetActiveContractsResponse encoding

* ideintifier conversion renaming

* PackageService resolveTemplateId returns domain.TemplateId now

* Resolving LF Identifier instead of Template ID, this should also work for Exercise command decoding

* cleaning up a bit

* daml-lf: show type in TypedValueGenerators-driven errors

* exercise command json encoding/decoding works

* command/exercise IOU_Transfer integration test passes now

* avoid filter for Gens; makes many contract ID gens not fail

* test ApiCodecCompressed against 100 random types, 20 random values each

* Updating README instructions

* improving error handling, failed futures, get logged and reported to the user now as 500

* [ROUTING DSL] Removing routing DSL, it did not work

* getting rid of HttpEntity.Strict match + cleanup

* fixing the merge conflict

* updating README

* use Show.shows instead of new Show

* List(_) isn't checked, but Seq(_) is slightly safer

* improving test assertions

* Adding /contracts/lookup implementation

* http-json: use ImmArraySeq instead of List; use toRightDisjuction

* http-json: .toList.toSet is shorter than fold

* http-json: replace .leftMap.map with .bimap

* http-json: use subst instead of reimplementing JsonFormat

* http-json: remove unused ExceptionHandler

* http-json: safer == comparison

* Adding two test cases for expected errors

* Adding BazelRunfiles.rlocation magic that supposed to handle windows path for bazel dependencies

* http-json: import, not extend
This commit is contained in:
Leonid Shlyapnikov 2019-07-29 16:49:57 -04:00 committed by GitHub
parent 0ffe5945b8
commit b940951a76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1933 additions and 318 deletions

View File

@ -34,6 +34,7 @@ object TypedValueGenerators {
def prj[Cid]: Value[Cid] => Option[Inj[Cid]]
def injgen[Cid](cid: Gen[Cid]): Gen[Inj[Cid]]
implicit def injshrink[Cid: Shrink]: Shrink[Inj[Cid]]
final override def toString = s"${classOf[ValueAddend].getSimpleName}{t = ${t.toString}}"
}
@SuppressWarnings(Array("org.wartremover.warts.Any"))

View File

@ -203,7 +203,9 @@ object ValueGenerators {
val genRel: Gen[ContractId] =
Arbitrary.arbInt.arbitrary.map(i => RelativeContractId(Transaction.NodeId.unsafeFromIndex(i)))
val genAbs: Gen[ContractId] =
Gen.alphaStr.filter(_.nonEmpty).map(s => AbsoluteContractId(toContractId(s)))
Gen.zip(Gen.alphaChar, Gen.alphaStr) map {
case (h, t) => AbsoluteContractId(toContractId(h +: t))
}
Gen.frequency((1, genRel), (3, genAbs))
}

View File

@ -296,7 +296,10 @@ daml_compile(
name = "quickstart-model",
srcs = glob(["source/getting-started/quickstart/template-root/**/*.daml"]),
main_src = "source/getting-started/quickstart/template-root/daml/Main.daml",
visibility = ["//language-support/scala/examples:__subpackages__"],
visibility = [
"//language-support/scala/examples:__subpackages__",
"//ledger-service:__subpackages__",
],
)
dar_to_java(

View File

@ -8,6 +8,11 @@ load(
"da_scala_test",
)
hj_scalacopts = [
"-Ypartial-unification",
"-Xsource:2.13",
]
http_json_deps = [
"//3rdparty/jvm/ch/qos/logback:logback_classic",
"//3rdparty/jvm/com/github/pureconfig",
@ -17,15 +22,20 @@ http_json_deps = [
"//3rdparty/jvm/com/typesafe/akka:akka_slf4j",
"//3rdparty/jvm/com/typesafe/scala_logging",
"//3rdparty/jvm/org/scalaz:scalaz_core",
"//ledger-service/utils",
"//daml-lf/interface",
"//daml-lf/transaction",
"//language-support/scala/bindings-akka",
"//ledger-api/rs-grpc-bridge",
"//ledger-service/lf-value-json",
"//ledger-service/utils",
"//ledger/ledger-api-common",
"//daml-lf/data:data",
]
da_scala_library(
name = "http-json",
srcs = glob(["src/main/scala/**/*.scala"]),
scalacopts = hj_scalacopts,
tags = ["maven_coordinates=com.digitalasset.ledger-service:http-json:__VERSION__"],
deps = http_json_deps,
)
@ -34,18 +44,24 @@ da_scala_binary(
name = "http-json-bin",
srcs = glob(["src/main/scala/**/*.scala"]),
main_class = "com.digitalasset.http.Main",
scalacopts = hj_scalacopts,
deps = [":http-json"] + http_json_deps,
)
da_scala_test(
name = "tests",
size = "small",
size = "medium",
srcs = glob(["src/test/scala/**/*.scala"]),
data = ["//docs:quickstart-model.dar"],
resources = glob(["src/test/resources/**/*"]),
scalacopts = hj_scalacopts,
deps = [
":http-json",
"//3rdparty/jvm/org/scalacheck",
"//3rdparty/jvm/org/scalaz:scalaz_scalacheck_binding",
"//3rdparty/jvm/org/scalatest:scalatest",
"//3rdparty/jvm/org/scalatest",
"//ledger/sandbox:sandbox",
"//ledger/participant-state",
"//bazel_tools/runfiles:scala_runfiles",
] + http_json_deps,
)

View File

@ -0,0 +1,133 @@
# HTTP JSON Service
## How to start
### Start sandbox from a DAML Assistant project directory
```
daml-head sandbox --wall-clock-time ./.daml/dist/quickstart.dar
```
### Start HTTP service from the DAML project root
This will build the service first, can take up to 5-10 minutes when running first time.
```
$ bazel run //ledger-service/http-json:http-json-bin -- localhost 6865 7575
```
Where:
- localhost 6865 -- sandbox host and port
- 7575 -- HTTP service port
## Example session
```
$ cd <daml-root>/
$ daml-sdk-head
$ cd $HOME
$ daml-head new iou-quickstart-java quickstart-java
$ cd iou-quickstart-java/
$ daml-head build
$ daml-head sandbox --wall-clock-time ./.daml/dist/quickstart.dar
cd <daml-root>/
$ bazel run //ledger-service/http-json:http-json-bin -- localhost 6865 7575
```
`Alice` party is hardcoded, the below assumes you are Alice:
### GET http://localhost:7575/contracts/search
### POST http://localhost:7575/contracts/search
application/json body:
```
{"templateIds": [{"moduleName": "Iou", "entityName": "Iou"}]}
```
### POST http://localhost:7575/command/create
application/json body:
```
{
"templateId": {
"moduleName": "Iou",
"entityName": "Iou"
},
"argument": {
"observers": [],
"issuer": "Alice",
"amount": "999.99",
"currency": "USD",
"owner": "Alice"
}
}
```
output:
```
{
"status": 200,
"result": {
"agreementText": "",
"contractId": "#20:0",
"templateId": {
"packageId": "bede798df37ce01fc402d266ae89d5bc4c61d70968b6a4f0baf69b24140579aa",
"moduleName": "Iou",
"entityName": "Iou"
},
"witnessParties": [
"Alice"
],
"argument": {
"observers": [],
"issuer": "Alice",
"amount": "999.99",
"currency": "USD",
"owner": "Alice"
}
}
}
```
### POST http://localhost:44279/command/exercise
`"contractId": "#20:0"` is the value from the create output
application/json body:
```
{
"templateId": {
"moduleName": "Iou",
"entityName": "Iou"
},
"contractId": "#20:0",
"choice": "Iou_Transfer",
"argument": {
"newOwner": "Alice"
}
}
```
output:
```
{
"status": 200,
"result": [
{
"agreementText": "",
"contractId": "#160:1",
"templateId": {
"packageId": "bede798df37ce01fc402d266ae89d5bc4c61d70968b6a4f0baf69b24140579aa",
"moduleName": "Iou",
"entityName": "IouTransfer"
},
"witnessParties": [
"Alice"
],
"argument": {
"iou": {
"observers": [],
"issuer": "Alice",
"amount": "999.99",
"currency": "USD",
"owner": "Alice"
},
"newOwner": "Alice"
}
}
]
}
```

View File

@ -0,0 +1,152 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http
import java.time.Instant
import com.digitalasset.api.util.TimeProvider
import com.digitalasset.daml.lf.data.ImmArray.ImmArraySeq
import com.digitalasset.http.CommandService.Error
import com.digitalasset.http.util.ClientUtil.{uniqueCommandId, workflowIdFromParty}
import com.digitalasset.http.util.IdentifierConverters.refApiIdentifier
import com.digitalasset.http.util.{Commands, Transactions}
import com.digitalasset.ledger.api.refinements.{ApiTypes => lar}
import com.digitalasset.ledger.api.{v1 => lav1}
import com.typesafe.scalalogging.StrictLogging
import scalaz.std.scalaFuture._
import scalaz.syntax.show._
import scalaz.syntax.std.option._
import scalaz.{-\/, EitherT, Show, \/, \/-}
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
class CommandService(
resolveTemplateId: PackageService.ResolveTemplateId,
submitAndWaitForTransaction: Services.SubmitAndWaitForTransaction,
timeProvider: TimeProvider,
defaultTimeToLive: Duration = 30.seconds)(implicit ec: ExecutionContext)
extends StrictLogging {
@SuppressWarnings(Array("org.wartremover.warts.Any"))
def create(jwtPayload: domain.JwtPayload, input: domain.CreateCommand[lav1.value.Record])
: Future[Error \/ domain.ActiveContract[lav1.value.Value]] = {
val et: EitherT[Future, Error, domain.ActiveContract[lav1.value.Value]] = for {
command <- EitherT.either(createCommand(input))
request = submitAndWaitRequest(jwtPayload, input.meta, command)
response <- liftET(logResult('create, submitAndWaitForTransaction(request)))
contract <- EitherT.either(exactlyOneActiveContract(response))
} yield contract
et.run
}
private def liftET[A](fa: Future[A]): EitherT[Future, Error, A] = EitherT.rightT(fa)
@SuppressWarnings(Array("org.wartremover.warts.Any"))
def exercise(jwtPayload: domain.JwtPayload, input: domain.ExerciseCommand[lav1.value.Record])
: Future[Error \/ ImmArraySeq[domain.ActiveContract[lav1.value.Value]]] = {
val et: EitherT[Future, Error, ImmArraySeq[domain.ActiveContract[lav1.value.Value]]] = for {
command <- EitherT.either(exerciseCommand(input))
request = submitAndWaitRequest(jwtPayload, input.meta, command)
response <- liftET(logResult('exercise, submitAndWaitForTransaction(request)))
contracts <- EitherT.either(activeContracts(response))
} yield contracts
et.run
}
def eitherT[A](fa: Future[A]): Future[Error \/ A] = fa.map(a => \/-(a))
private def logResult[A](op: Symbol, fa: Future[A]): Future[A] = {
fa.onComplete {
case Failure(e) => logger.error(s"$op failure", e)
case Success(a) => logger.debug(s"$op success: $a")
}
fa
}
private def createCommand(input: domain.CreateCommand[lav1.value.Record])
: Error \/ lav1.commands.Command.Command.Create = {
resolveTemplateId(input.templateId)
.bimap(
e => Error('createCommand, e.shows),
x => Commands.create(refApiIdentifier(x), input.argument))
}
private def exerciseCommand(input: domain.ExerciseCommand[lav1.value.Record])
: Error \/ lav1.commands.Command.Command.Exercise = {
resolveTemplateId(input.templateId)
.bimap(
e => Error('exerciseCommand, e.shows),
x => Commands.exercise(refApiIdentifier(x), input.contractId, input.choice, input.argument))
}
private def submitAndWaitRequest(
jwtPayload: domain.JwtPayload,
meta: Option[domain.CommandMeta],
command: lav1.commands.Command.Command): lav1.command_service.SubmitAndWaitRequest = {
val ledgerEffectiveTime: Instant =
meta.flatMap(_.ledgerEffectiveTime).getOrElse(timeProvider.getCurrentTime)
val maximumRecordTime: Instant = meta
.flatMap(_.maximumRecordTime)
.getOrElse(ledgerEffectiveTime.plusNanos(defaultTimeToLive.toNanos))
val workflowId: lar.WorkflowId =
meta.flatMap(_.workflowId).getOrElse(workflowIdFromParty(jwtPayload.party))
val commandId: lar.CommandId = meta.flatMap(_.commandId).getOrElse(uniqueCommandId())
Commands.submitAndWaitRequest(
jwtPayload.ledgerId,
jwtPayload.applicationId,
workflowId,
commandId,
ledgerEffectiveTime,
maximumRecordTime,
jwtPayload.party,
command
)
}
private def exactlyOneActiveContract(
response: lav1.command_service.SubmitAndWaitForTransactionResponse)
: Error \/ domain.ActiveContract[lav1.value.Value] =
activeContracts(response).flatMap {
case Seq(x) => \/-(x)
case xs @ _ =>
-\/(Error('exactlyOneActiveContract, s"Expected exactly one active contract, got: $xs"))
}
private def activeContracts(response: lav1.command_service.SubmitAndWaitForTransactionResponse)
: Error \/ ImmArraySeq[domain.ActiveContract[lav1.value.Value]] =
response.transaction
.toRightDisjunction(
Error('activeContracts, s"Received response without transaction: $response"))
.flatMap(activeContracts)
@SuppressWarnings(Array("org.wartremover.warts.Any"))
private def activeContracts(tx: lav1.transaction.Transaction)
: Error \/ ImmArraySeq[domain.ActiveContract[lav1.value.Value]] = {
import scalaz.syntax.traverse._
Transactions
.decodeAllCreatedEvents(tx)
.traverse(domain.ActiveContract.fromLedgerApi)
.leftMap(e => Error('activeContracts, e))
}
}
object CommandService {
case class Error(id: Symbol, message: String)
object Error {
implicit val errorShow: Show[Error] = Show shows { e =>
s"CommandService Error, ${e.id: Symbol}: ${e.message: String}"
}
}
}

View File

@ -5,36 +5,76 @@ package com.digitalasset.http
import akka.stream.Materializer
import akka.stream.scaladsl.Sink
import com.digitalasset.http.ContractsService._
import com.digitalasset.http.domain.TemplateId
import com.digitalasset.http.util.FutureUtil.toFuture
import com.digitalasset.ledger.api.v1.transaction_filter.{
Filters,
InclusiveFilters,
TransactionFilter
}
import com.digitalasset.ledger.api.v1.value.{Identifier, Value}
import com.digitalasset.http.util.IdentifierConverters.apiIdentifier
import com.digitalasset.ledger.api.refinements.{ApiTypes => lar}
import com.digitalasset.ledger.api.{v1 => lav1}
import com.digitalasset.ledger.client.services.acs.ActiveContractSetClient
import scalaz.\/
import scalaz.std.string._
import scalaz.{-\/, \/-}
import scala.concurrent.{ExecutionContext, Future}
class ContractsService(
resolveTemplateIds: ResolveTemplateIds,
resolveTemplateIds: PackageService.ResolveTemplateIds,
activeContractSetClient: ActiveContractSetClient,
parallelism: Int = 8)(implicit ec: ExecutionContext, mat: Materializer) {
def lookup(jwtPayload: domain.JwtPayload, request: domain.ContractLookupRequest[lav1.value.Value])
: Future[Option[domain.ActiveContract[lav1.value.Value]]] =
request.id match {
case -\/((templateId, contractKey)) =>
lookup(jwtPayload.party, templateId, contractKey)
case \/-((templateId, contractId)) =>
lookup(jwtPayload.party, templateId, contractId)
}
def lookup(
jwtPayload: domain.JwtPayload,
request: domain.ContractLookupRequest[Value]): Future[Option[domain.ActiveContract[Value]]] =
Future.failed(new RuntimeException("contract lookup not yet supported")) // TODO
party: lar.Party,
templateId: TemplateId.OptionalPkg,
contractKey: lav1.value.Value): Future[Option[domain.ActiveContract[lav1.value.Value]]] =
for {
as <- search(party, Set(templateId))
a = findByContractKey(contractKey)(as)
} yield a
private def findByContractKey(k: lav1.value.Value)(
as: Seq[domain.GetActiveContractsResponse[lav1.value.Value]])
: Option[domain.ActiveContract[lav1.value.Value]] =
(as.view: Seq[domain.GetActiveContractsResponse[lav1.value.Value]])
.flatMap(a => a.activeContracts)
.find(isContractKey(k))
private def isContractKey(k: lav1.value.Value)(
a: domain.ActiveContract[lav1.value.Value]): Boolean =
a.key.fold(false)(_ == k)
def lookup(
party: lar.Party,
templateId: Option[TemplateId.OptionalPkg],
contractId: String): Future[Option[domain.ActiveContract[lav1.value.Value]]] =
for {
as <- search(party, templateIds(templateId))
a = findByContractId(contractId)(as)
} yield a
private def templateIds(a: Option[TemplateId.OptionalPkg]): Set[TemplateId.OptionalPkg] =
a.toList.toSet
private def findByContractId(k: String)(
as: Seq[domain.GetActiveContractsResponse[lav1.value.Value]])
: Option[domain.ActiveContract[lav1.value.Value]] =
(as.view: Seq[domain.GetActiveContractsResponse[lav1.value.Value]])
.flatMap(a => a.activeContracts)
.find(x => (x.contractId: String) == k)
def search(jwtPayload: domain.JwtPayload, request: domain.GetActiveContractsRequest)
: Future[Seq[domain.GetActiveContractsResponse[Value]]] =
: Future[Seq[domain.GetActiveContractsResponse[lav1.value.Value]]] =
search(jwtPayload.party, request.templateIds)
def search(party: String, templateIds: Set[domain.TemplateId.OptionalPkg])
: Future[Seq[domain.GetActiveContractsResponse[Value]]] =
def search(party: lar.Party, templateIds: Set[domain.TemplateId.OptionalPkg])
: Future[Seq[domain.GetActiveContractsResponse[lav1.value.Value]]] =
for {
templateIds <- toFuture(resolveTemplateIds(templateIds))
activeContracts <- activeContractSetClient
@ -44,17 +84,19 @@ class ContractsService(
.runWith(Sink.seq)
} yield activeContracts
private def transactionFilter(party: String, templateIds: List[Identifier]): TransactionFilter = {
private def transactionFilter(
party: lar.Party,
templateIds: List[TemplateId.RequiredPkg]): lav1.transaction_filter.TransactionFilter = {
import lav1.transaction_filter._
val filters =
if (templateIds.isEmpty) Filters.defaultInstance
else Filters(Some(InclusiveFilters(templateIds)))
TransactionFilter(Map(party -> filters))
else Filters(Some(lav1.transaction_filter.InclusiveFilters(templateIds.map(apiIdentifier))))
TransactionFilter(Map(lar.Party.unwrap(party) -> filters))
}
}
object ContractsService {
final case class Error(message: String)
type ResolveTemplateIds =
Set[domain.TemplateId.OptionalPkg] => PackageService.Error \/ List[Identifier]
}

View File

@ -5,165 +5,188 @@ package com.digitalasset.http
import akka.http.scaladsl.model.HttpMethods.{GET, POST}
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.stream.scaladsl.Source
import akka.util.ByteString
import com.digitalasset.http.json.JsonProtocol._
import com.digitalasset.ledger.api.v1.value.Value
import com.digitalasset.http.json.ResponseFormats._
import com.digitalasset.http.json.SprayJson.decode
import com.digitalasset.http.json.{DomainJsonDecoder, DomainJsonEncoder, SprayJson}
import com.digitalasset.ledger.api.refinements.{ApiTypes => lar}
import com.digitalasset.ledger.api.{v1 => lav1}
import com.typesafe.scalalogging.StrictLogging
import scalaz.{-\/, @@, \/, \/-}
import scalaz.syntax.functor._
import scalaz.std.list._
import scalaz.syntax.show._
import scalaz.syntax.traverse._
import scalaz.{-\/, Show, \/, \/-}
import spray.json._
import json.HttpCodec._
import json.ResponseFormats._
import akka.http.scaladsl.server.{Directive, Directive1, Route}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
import scala.util.control.NonFatal
class Endpoints(contractsService: ContractsService)(implicit ec: ExecutionContext)
@SuppressWarnings(Array("org.wartremover.warts.Any"))
class Endpoints(
ledgerId: lar.LedgerId,
commandService: CommandService,
contractsService: ContractsService,
encoder: DomainJsonEncoder,
decoder: DomainJsonDecoder)(implicit ec: ExecutionContext)
extends StrictLogging {
import Endpoints._
import json.JsonProtocol._
// TODO(Leo) read it from the header
private val jwtPayload =
domain.JwtPayload(ledgerId = "ledgerId", applicationId = "applicationId", party = "Alice")
private val extractJwtPayload: Directive1[domain.JwtPayload] =
Directive(_(Tuple1(jwtPayload))) // TODO from header
private val ok = HttpEntity(ContentTypes.`application/json`, """{"status": "OK"}""")
domain.JwtPayload(ledgerId, lar.ApplicationId("applicationId"), lar.Party("Alice"))
lazy val all: PartialFunction[HttpRequest, HttpResponse] =
command orElse contracts orElse notFound
lazy val all2: Route = command2 ~ contracts2
lazy val command: PartialFunction[HttpRequest, HttpResponse] = {
case HttpRequest(
POST,
Uri.Path("/command/create"),
_,
HttpEntity.Strict(ContentTypes.`application/json`, data),
_) =>
HttpResponse(entity = HttpEntity.Strict(ContentTypes.`application/json`, data))
case req @ HttpRequest(POST, Uri.Path("/command/create"), _, _, _) =>
httpResponse(
input(req)
.map(decoder.decodeR[domain.CreateCommand])
.mapAsync(1) {
case -\/(e) => invalidUserInput(e)
case \/-(c) => handleFutureFailure(commandService.create(jwtPayload, c))
}
.map { fa =>
fa.flatMap { a: domain.ActiveContract[lav1.value.Value] =>
encoder.encodeV(a).leftMap(e => ServerError(e.shows))
}
}
.map(formatResult)
)
case HttpRequest(
POST,
Uri.Path("/command/exercise"),
_,
HttpEntity.Strict(ContentTypes.`application/json`, data),
_) =>
HttpResponse(entity = HttpEntity.Strict(ContentTypes.`application/json`, data))
case req @ HttpRequest(POST, Uri.Path("/command/exercise"), _, _, _) =>
httpResponse(
input(req)
.map(decoder.decodeR[domain.ExerciseCommand])
.mapAsync(1) {
case -\/(e) => invalidUserInput(e)
case \/-(c) => handleFutureFailure(commandService.exercise(jwtPayload, c))
}
.map { fa =>
fa.flatMap { as =>
as.traverse(a => encoder.encodeV(a))
.leftMap(e => ServerError(e.shows))
.flatMap(as => encodeList(as))
}
}
.map(formatResult)
)
}
lazy val command2: Route =
post {
path("/command/create") {
entity(as[JsValue]) { data =>
complete(data)
}
} ~
path("/command/exercise") {
entity(as[JsValue]) { data =>
complete(data)
private def invalidUserInput[A: Show, B](a: A): Future[InvalidUserInput \/ B] =
Future.successful(-\/(InvalidUserInput(a.shows)))
private def handleFutureFailure[A: Show, B](fa: Future[A \/ B]): Future[ServerError \/ B] =
fa.map(a => a.leftMap(e => ServerError(e.shows))).recover {
case NonFatal(e) =>
logger.error("Future failed", e)
-\/(ServerError(e.getMessage))
}
private def handleFutureFailure[A](fa: Future[A]): Future[ServerError \/ A] =
fa.map(a => \/-(a)).recover {
case NonFatal(e) =>
logger.error("Future failed", e)
-\/(ServerError(e.getMessage))
}
private def encodeList(as: Seq[JsValue]): ServerError \/ JsValue =
SprayJson.encode(as).leftMap(e => ServerError(e.shows))
private def formatResult(fa: Error \/ JsValue): ByteString = fa match {
case \/-(a) => format(resultJsObject(a: JsValue))
case -\/(InvalidUserInput(e)) => format(errorsJsObject(StatusCodes.BadRequest)(e))
case -\/(ServerError(e)) => format(errorsJsObject(StatusCodes.InternalServerError)(e))
}
private val emptyGetActiveContractsRequest = domain.GetActiveContractsRequest(Set.empty)
lazy val contracts: PartialFunction[HttpRequest, HttpResponse] = {
case HttpRequest(GET, Uri.Path("/contracts/lookup"), _, _, _) =>
HttpResponse(entity = ok)
case req @ HttpRequest(GET, Uri.Path("/contracts/lookup"), _, _, _) =>
httpResponse(
input(req)
.map(decoder.decodeV[domain.ContractLookupRequest])
.mapAsync(1) {
case -\/(e) => invalidUserInput(e)
case \/-(c) => handleFutureFailure(contractsService.lookup(jwtPayload, c))
}
.map { fa =>
fa.flatMap {
case None => \/-(JsObject())
case Some(x) => encoder.encodeV(x).leftMap(e => ServerError(e.shows))
}
}
.map(formatResult)
)
case HttpRequest(GET, Uri.Path("/contracts/search"), _, _, _) =>
httpResponse(
contractsService
.search(jwtPayload, emptyGetActiveContractsRequest)
.map(x => format(resultJsObject(x.toString)))
handleFutureFailure(contractsService.search(jwtPayload, emptyGetActiveContractsRequest))
.map { fas: Error \/ Seq[domain.GetActiveContractsResponse[lav1.value.Value]] =>
fas.flatMap { as =>
as.toList
.traverse(a => encoder.encodeV(a))
.leftMap(e => ServerError(e.shows))
.flatMap(js => encodeList(js))
}
}
.map(formatResult)
)
case HttpRequest(POST, Uri.Path("/contracts/search"), _, HttpEntity.Strict(_, input), _) =>
parse[domain.GetActiveContractsRequest](input) match {
case -\/(e) =>
httpResponse(StatusCodes.BadRequest, format(errorsJsObject(StatusCodes.BadRequest)(e)))
case \/-(a) =>
case req @ HttpRequest(POST, Uri.Path("/contracts/search"), _, _, _) =>
httpResponse(
contractsService.search(jwtPayload, a).map(x => format(resultJsObject(x.toString)))
input(req)
.map(decode[domain.GetActiveContractsRequest])
.mapAsync(1) {
case -\/(e) => invalidUserInput(e)
case \/-(c) => handleFutureFailure(contractsService.search(jwtPayload, c))
}
.map { fas: Error \/ Seq[domain.GetActiveContractsResponse[lav1.value.Value]] =>
fas.flatMap { as =>
as.toList
.traverse(a => encoder.encodeV(a))
.leftMap(e => ServerError(e.shows))
.flatMap(js => encodeList(js))
}
}
.map(formatResult)
)
}
}
lazy val contracts2: Route =
path("/contracts/lookup") {
post {
extractJwtPayload { jwt =>
entity(as[domain.ContractLookupRequest[JsValue] @@ JsonApi]) {
case JsonApi(clr) =>
// TODO SC: the result gets labelled "result" not "contract"; does this matter?
complete(
JsonApi.subst(
contractsService
.lookup(jwt, clr map placeholderLfValueDec)
.map(oac => resultJsObject(oac.map(_.map(placeholderLfValueEnc))))))
}
}
}
} ~
path("contracts/search") {
get {
extractJwtPayload { jwt =>
complete(
JsonApi.subst(
contractsService
.search(jwt, emptyGetActiveContractsRequest)
.map(sgacr => resultJsObject(sgacr.map(_.map(placeholderLfValueEnc))))))
}
} ~
post {
extractJwtPayload { jwt =>
entity(as[domain.GetActiveContractsRequest @@ JsonApi]) {
case JsonApi(gacr) =>
complete(
JsonApi.subst(contractsService
.search(jwt, gacr)
.map(sgacr => resultJsObject(sgacr.map(_.map(placeholderLfValueEnc))))))
}
}
}
}
private def httpResponse(status: StatusCode, output: ByteString): HttpResponse =
HttpResponse(
status = status,
entity = HttpEntity.Strict(ContentTypes.`application/json`, output))
private def httpResponse(output: Future[ByteString]): HttpResponse =
HttpResponse(entity =
HttpEntity.CloseDelimited(ContentTypes.`application/json`, Source.fromFuture(output)))
// TODO SC: this is a placeholder because we can't do this accurately
// without type context
private def placeholderLfValueEnc(v: Value): JsValue =
JsString(v.sum.toString)
// TODO SC: this is a placeholder because we can't do this accurately
// without type context
private def placeholderLfValueDec(v: JsValue): Value =
Value(Value.Sum.Text(v.toString))
private def httpResponse(output: Source[ByteString, _]): HttpResponse =
HttpResponse(entity = HttpEntity.CloseDelimited(ContentTypes.`application/json`, output))
lazy val notFound: PartialFunction[HttpRequest, HttpResponse] = {
case HttpRequest(_, _, _, _, _) => HttpResponse(status = StatusCodes.NotFound)
}
private def parse[A: JsonReader](str: ByteString): String \/ A =
Try {
val jsonAst: JsValue = str.utf8String.parseJson
jsonAst.convertTo[A]
} fold (t => \/.left(s"JSON parser error: ${t.getMessage}"), a => \/.right(a))
private def format(a: JsValue): ByteString =
ByteString(a.compactPrint)
}
object Endpoints {
sealed abstract class Error(message: String) extends Product with Serializable
final case class InvalidUserInput(message: String) extends Error(message)
final case class ServerError(message: String) extends Error(message)
object Error {
implicit val ShowInstance: Show[Error] = Show shows {
case InvalidUserInput(message) => s"InvalidUserInput: ${message: String}"
case ServerError(message) => s"ServerError: ${message: String}"
}
}
private[http] def input(req: HttpRequest): Source[String, _] =
req.entity.dataBytes.fold(ByteString.empty)(_ ++ _).map(_.utf8String).take(1L)
}

View File

@ -8,15 +8,28 @@ import akka.http.scaladsl.Http
import akka.http.scaladsl.Http.ServerBinding
import akka.stream.Materializer
import akka.stream.scaladsl.Flow
import com.digitalasset.api.util.TimeProvider
import com.digitalasset.grpc.adapter.ExecutionSequencerFactory
import com.digitalasset.http.PackageService.TemplateIdMap
import com.digitalasset.http.json.{
ApiValueToJsValueConverter,
DomainJsonDecoder,
DomainJsonEncoder,
JsValueToApiValueConverter
}
import com.digitalasset.http.util.ApiValueToLfValueConverter
import com.digitalasset.http.util.FutureUtil._
import com.digitalasset.http.util.IdentifierConverters.apiLedgerId
import com.digitalasset.ledger.api.refinements.ApiTypes.ApplicationId
import com.digitalasset.ledger.api.refinements.{ApiTypes => lar}
import com.digitalasset.ledger.client.LedgerClient
import com.digitalasset.ledger.client.configuration.{
CommandClientConfiguration,
LedgerClientConfiguration,
LedgerIdRequirement
}
import com.digitalasset.ledger.service.LedgerReader
import com.digitalasset.ledger.service.LedgerReader.PackageStore
import com.typesafe.scalalogging.StrictLogging
import scalaz.Scalaz._
import scalaz._
@ -46,14 +59,36 @@ object HttpService extends StrictLogging {
val bindingS: EitherT[Future, Error, ServerBinding] = for {
client <- liftET[Error](
LedgerClient.singleHost(ledgerHost, ledgerPort, clientConfig)(ec, aesf))
packageService = new PackageService(client.packageClient)
templateIdMap <- eitherT(packageService.getTemplateIdMap()).leftMap(httpServiceError)
ledgerId = apiLedgerId(client.ledgerId): lar.LedgerId
packageStore <- eitherT(LedgerReader.createPackageStore(client.packageClient))
.leftMap(httpServiceError)
templateIdMap = PackageService.getTemplateIdMap(packageStore)
commandService = new CommandService(
PackageService.resolveTemplateId(templateIdMap),
client.commandServiceClient.submitAndWaitForTransaction,
TimeProvider.UTC)
contractsService = new ContractsService(
PackageService.resolveTemplateIds(templateIdMap),
client.activeContractSetClient)
endpoints = new Endpoints(contractsService)
(encoder, decoder) = buildJsonCodecs(ledgerId, packageStore, templateIdMap)
endpoints = new Endpoints(
ledgerId,
commandService,
contractsService,
encoder,
decoder,
)
binding <- liftET[Error](
Http().bindAndHandle(Flow.fromFunction(endpoints.all), "localhost", httpPort))
} yield binding
val bindingF: Future[Error \/ ServerBinding] = bindingS.run
@ -67,10 +102,31 @@ object HttpService extends StrictLogging {
bindingF
}
private def httpServiceError(e: PackageService.Error): Error = Error(e.shows)
private def httpServiceError(e: String): Error = Error(e)
def stop(f: Future[Error \/ ServerBinding])(implicit ec: ExecutionContext): Future[Unit] = {
logger.info("Stopping server...")
f.collect { case \/-(a) => a.unbind() }.join
}
private[http] def buildJsonCodecs(
ledgerId: lar.LedgerId,
packageStore: PackageStore,
templateIdMap: TemplateIdMap): (DomainJsonEncoder, DomainJsonDecoder) = {
val resolveTemplateId = PackageService.resolveTemplateId(templateIdMap) _
val lfTypeLookup = LedgerReader.damlLfTypeLookup(packageStore) _
val jsValueToApiValueConverter = new JsValueToApiValueConverter(lfTypeLookup)
val jsObjectToApiRecord = jsValueToApiValueConverter.jsObjectToApiRecord _
val jsValueToApiValue = jsValueToApiValueConverter.jsValueToApiValue _
val apiValueToJsValueConverter = new ApiValueToJsValueConverter(
ApiValueToLfValueConverter.apiValueToLfValue)
val apiValueToJsValue = apiValueToJsValueConverter.apiValueToJsValue _
val apiRecordToJsObject = apiValueToJsValueConverter.apiRecordToJsObject _
val encoder = new DomainJsonEncoder(apiRecordToJsObject, apiValueToJsValue)
val decoder = new DomainJsonDecoder(resolveTemplateId, jsObjectToApiRecord, jsValueToApiValue)
(encoder, decoder)
}
}

View File

@ -3,86 +3,84 @@
package com.digitalasset.http
import com.digitalasset.ledger.api.v1.value.Identifier
import com.digitalasset.ledger.client.services.pkg.PackageClient
import com.digitalasset.ledger.service.{LedgerReader, TemplateIds}
import com.digitalasset.http.domain.TemplateId
import com.digitalasset.ledger.service.LedgerReader.PackageStore
import com.digitalasset.ledger.service.TemplateIds
import scalaz.Scalaz._
import scalaz._
import scala.collection.breakOut
import scala.concurrent.{ExecutionContext, Future}
class PackageService(packageClient: PackageClient)(implicit ec: ExecutionContext) {
import PackageService._
def getTemplateIdMap(): Future[Error \/ TemplateIdMap] =
EitherT(LedgerReader.createPackageStore(packageClient))
.leftMap(e => ServerError(e))
.map { packageStore =>
val templateIds = TemplateIds.getTemplateIds(packageStore.values.toSet)
buildTemplateIdMap(templateIds)
}
.run
}
object PackageService {
sealed trait Error
final case class InputError(message: String) extends Error
final case class ServerError(message: String) extends Error
implicit val errorShow: Show[Error] = new Show[Error] {
override def shows(e: Error): String = e match {
object Error {
implicit val errorShow: Show[Error] = Show shows {
case InputError(m) => s"PackageService input error: ${m: String}"
case ServerError(m) => s"PackageService server error: ${m: String}"
}
}
case class TemplateIdMap(
all: Map[domain.TemplateId.RequiredPkg, Identifier],
unique: Map[domain.TemplateId.NoPkg, Identifier])
type ResolveTemplateIds =
Set[domain.TemplateId.OptionalPkg] => Error \/ List[TemplateId.RequiredPkg]
def buildTemplateIdMap(ids: Set[Identifier]): TemplateIdMap = {
val all: Map[domain.TemplateId.RequiredPkg, Identifier] = ids.map(a => key3(a) -> a)(breakOut)
val unique: Map[domain.TemplateId.NoPkg, Identifier] = filterUniqueTemplateIs(all)
type ResolveTemplateId =
domain.TemplateId.OptionalPkg => Error \/ TemplateId.RequiredPkg
def getTemplateIdMap(packageStore: PackageStore): TemplateIdMap =
buildTemplateIdMap(collectTemplateIds(packageStore))
private def collectTemplateIds(packageStore: PackageStore): Set[TemplateId.RequiredPkg] =
TemplateIds
.getTemplateIds(packageStore.values.toSet)
.map(x => TemplateId(x.packageId, x.moduleName, x.entityName))
case class TemplateIdMap(
all: Set[TemplateId.RequiredPkg],
unique: Map[TemplateId.NoPkg, TemplateId.RequiredPkg])
def buildTemplateIdMap(ids: Set[TemplateId.RequiredPkg]): TemplateIdMap = {
val all: Set[TemplateId.RequiredPkg] = ids
val unique: Map[TemplateId.NoPkg, TemplateId.RequiredPkg] = filterUniqueTemplateIs(all)
TemplateIdMap(all, unique)
}
private[http] def key3(a: Identifier): domain.TemplateId.RequiredPkg =
domain.TemplateId[String](a.packageId, a.moduleName, a.entityName)
private[http] def key2(k: TemplateId.RequiredPkg): TemplateId.NoPkg =
TemplateId[Unit]((), k.moduleName, k.entityName)
private[http] def key2(k: domain.TemplateId.RequiredPkg): domain.TemplateId.NoPkg =
domain.TemplateId[Unit]((), k.moduleName, k.entityName)
private def filterUniqueTemplateIs(all: Map[domain.TemplateId.RequiredPkg, Identifier])
: Map[domain.TemplateId.NoPkg, Identifier] =
private def filterUniqueTemplateIs(
all: Set[TemplateId.RequiredPkg]): Map[TemplateId.NoPkg, TemplateId.RequiredPkg] =
all
.groupBy { case (k, _) => key2(k) }
.collect { case (k, v) if v.size == 1 => (k, v.values.head) }
.groupBy(k => key2(k))
.collect { case (k, v) if v.size == 1 => (k, v.head) }
@SuppressWarnings(Array("org.wartremover.warts.Any"))
def resolveTemplateIds(m: TemplateIdMap)(
as: Set[domain.TemplateId.OptionalPkg]): Error \/ List[Identifier] =
as: Set[TemplateId.OptionalPkg]): Error \/ List[TemplateId.RequiredPkg] =
for {
bs <- as.toList.traverseU(resolveTemplateId(m))
bs <- as.toList.traverse(resolveTemplateId(m))
_ <- validate(as, bs)
} yield bs
def resolveTemplateId(m: TemplateIdMap)(a: domain.TemplateId.OptionalPkg): Error \/ Identifier =
def resolveTemplateId(m: TemplateIdMap)(
a: TemplateId.OptionalPkg): Error \/ TemplateId.RequiredPkg =
a.packageId match {
case Some(p) => findTemplateIdByK3(m.all)(domain.TemplateId(p, a.moduleName, a.entityName))
case None => findTemplateIdByK2(m.unique)(domain.TemplateId((), a.moduleName, a.entityName))
case Some(p) => findTemplateIdByK3(m.all)(TemplateId(p, a.moduleName, a.entityName))
case None => findTemplateIdByK2(m.unique)(TemplateId((), a.moduleName, a.entityName))
}
private def findTemplateIdByK3(m: Map[domain.TemplateId.RequiredPkg, Identifier])(
k: domain.TemplateId.RequiredPkg): Error \/ Identifier =
m.get(k).toRightDisjunction(InputError(s"Cannot resolve ${k.toString}"))
private def findTemplateIdByK3(m: Set[TemplateId.RequiredPkg])(
k: TemplateId.RequiredPkg): Error \/ TemplateId.RequiredPkg =
if (m.contains(k)) \/-(k)
else -\/(InputError(s"Cannot resolve ${k.toString}"))
private def findTemplateIdByK2(m: Map[domain.TemplateId.NoPkg, Identifier])(
k: domain.TemplateId.NoPkg): Error \/ Identifier =
private def findTemplateIdByK2(m: Map[TemplateId.NoPkg, TemplateId.RequiredPkg])(
k: TemplateId.NoPkg): Error \/ TemplateId.RequiredPkg =
m.get(k).toRightDisjunction(InputError(s"Cannot resolve ${k.toString}"))
private def validate(
requested: Set[domain.TemplateId.OptionalPkg],
resolved: List[Identifier]): Error \/ Unit =
requested: Set[TemplateId.OptionalPkg],
resolved: List[TemplateId.RequiredPkg]): Error \/ Unit =
if (requested.size == resolved.size) \/.right(())
else
\/.left(

View File

@ -0,0 +1,14 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http
import com.digitalasset.ledger.api.{v1 => lav1}
import scala.concurrent.Future
object Services {
type SubmitAndWaitForTransaction =
lav1.command_service.SubmitAndWaitRequest => Future[
lav1.command_service.SubmitAndWaitForTransactionResponse]
}

View File

@ -3,18 +3,28 @@
package com.digitalasset.http
import com.digitalasset.ledger.api.{v1 => lav1}
import java.time.Instant
import scalaz.{Functor, \/}
import com.digitalasset.daml.lf
import com.digitalasset.daml.lf.data.Ref
import com.digitalasset.http.util.IdentifierConverters
import com.digitalasset.ledger.api.refinements.{ApiTypes => lar}
import com.digitalasset.ledger.api.{v1 => lav1}
import scalaz.std.list._
import scalaz.std.option._
import scalaz.std.tuple._
import scalaz.std.vector._
import scalaz.syntax.std.option._
import scalaz.syntax.traverse._
import scalaz.{-\/, Applicative, Traverse, \/, \/-}
import scala.language.higherKinds
@SuppressWarnings(Array("org.wartremover.warts.Any"))
object domain {
type Error = String
case class JwtPayload(ledgerId: String, applicationId: String, party: String)
case class JwtPayload(ledgerId: lar.LedgerId, applicationId: lar.ApplicationId, party: lar.Party)
case class TemplateId[+PkgId](packageId: PkgId, moduleName: String, entityName: String)
@ -62,16 +72,65 @@ object domain {
agreementText = in.agreementText getOrElse ""
)
implicit val covariant: Functor[ActiveContract] = new Functor[ActiveContract] {
override def map[A, B](fa: ActiveContract[A])(f: A => B) =
implicit val covariant: Traverse[ActiveContract] = new Traverse[ActiveContract] {
override def map[A, B](fa: ActiveContract[A])(f: A => B): ActiveContract[B] =
fa.copy(key = fa.key map f, argument = f(fa.argument))
override def traverseImpl[G[_]: Applicative, A, B](fa: ActiveContract[A])(
f: A => G[B]): G[ActiveContract[B]] = {
import scalaz.syntax.apply._
val gk: G[Option[B]] = fa.key traverse f
val ga: G[B] = f(fa.argument)
^(gk, ga)((k, a) => fa.copy(key = k, argument = a))
}
}
implicit val hasTemplateId: HasTemplateId[ActiveContract] = new HasTemplateId[ActiveContract] {
override def templateId(fa: ActiveContract[_]): TemplateId.OptionalPkg =
TemplateId(
Some(fa.templateId.packageId),
fa.templateId.moduleName,
fa.templateId.entityName)
override def lfIdentifier(
fa: ActiveContract[_],
templateId: TemplateId.RequiredPkg): lf.data.Ref.Identifier =
IdentifierConverters.lfIdentifier(templateId)
}
}
object ContractLookupRequest {
implicit val covariant: Functor[ContractLookupRequest] = new Functor[ContractLookupRequest] {
implicit val covariant: Traverse[ContractLookupRequest] = new Traverse[ContractLookupRequest] {
override def map[A, B](fa: ContractLookupRequest[A])(f: A => B) =
fa.copy(id = fa.id leftMap (_ map f))
override def traverseImpl[G[_]: Applicative, A, B](fa: ContractLookupRequest[A])(
f: A => G[B]): G[ContractLookupRequest[B]] = {
val G: Applicative[G] = implicitly
fa.id match {
case -\/(a) =>
a.traverse(f).map(b => fa.copy(id = -\/(b)))
case \/-(a) =>
// TODO: we don't actually need to copy it, just need to adjust the type for the left side
G.point(fa.copy(id = \/-(a)))
}
}
}
implicit val hasTemplateId: HasTemplateId[ContractLookupRequest] =
new HasTemplateId[ContractLookupRequest] {
override def templateId(fa: ContractLookupRequest[_]): TemplateId.OptionalPkg =
fa.id match {
case -\/((a, _)) => a
case \/-((Some(a), _)) => a
case \/-((None, _)) => TemplateId(None, "", "")
}
override def lfIdentifier(
fa: ContractLookupRequest[_],
templateId: TemplateId.RequiredPkg): Ref.Identifier =
IdentifierConverters.lfIdentifier(templateId)
}
}
@ -79,21 +138,83 @@ object domain {
def fromLedgerApi(in: lav1.active_contracts_service.GetActiveContractsResponse)
: Error \/ GetActiveContractsResponse[lav1.value.Value] =
for {
activeContracts <- in.activeContracts.toVector traverseU (ActiveContract.fromLedgerApi(_))
activeContracts <- in.activeContracts.toVector traverse (ActiveContract.fromLedgerApi(_))
} yield
GetActiveContractsResponse(
offset = in.offset,
workflowId = Some(in.workflowId) filter (_.nonEmpty),
activeContracts = activeContracts)
implicit val covariant: Functor[GetActiveContractsResponse] =
new Functor[GetActiveContractsResponse] {
override def map[A, B](fa: GetActiveContractsResponse[A])(f: A => B) =
fa copy (activeContracts = fa.activeContracts map (_ map f))
implicit val covariant: Traverse[GetActiveContractsResponse] =
new Traverse[GetActiveContractsResponse] {
override def traverseImpl[G[_]: Applicative, A, B](fa: GetActiveContractsResponse[A])(
f: A => G[B]): G[GetActiveContractsResponse[B]] = {
val gas: G[List[ActiveContract[B]]] =
fa.activeContracts.toList.traverse(a => a.traverse(f))
gas.map(as => fa.copy(activeContracts = as))
}
}
}
private[this] implicit final class ErrorOps[A](private val o: Option[A]) extends AnyVal {
def required(label: String): Error \/ A = o toRightDisjunction s"Missing required field $label"
}
final case class CommandMeta(
workflowId: Option[lar.WorkflowId],
commandId: Option[lar.CommandId],
ledgerEffectiveTime: Option[Instant],
maximumRecordTime: Option[Instant])
final case class CreateCommand[+LfV](
templateId: TemplateId.OptionalPkg,
argument: LfV,
meta: Option[CommandMeta])
final case class ExerciseCommand[+LfV](
templateId: TemplateId.OptionalPkg,
contractId: lar.ContractId,
choice: lar.Choice,
argument: LfV,
meta: Option[CommandMeta])
trait HasTemplateId[F[_]] {
def templateId(fa: F[_]): TemplateId.OptionalPkg
def lfIdentifier(fa: F[_], templateId: TemplateId.RequiredPkg): lf.data.Ref.Identifier
}
object CreateCommand {
implicit val traverseInstance: Traverse[CreateCommand] = new Traverse[CreateCommand] {
override def traverseImpl[G[_]: Applicative, A, B](fa: CreateCommand[A])(
f: A => G[B]): G[CreateCommand[B]] =
f(fa.argument).map(a => fa.copy(argument = a))
}
implicit val hasTemplateId: HasTemplateId[CreateCommand] = new HasTemplateId[CreateCommand] {
override def templateId(fa: CreateCommand[_]): TemplateId.OptionalPkg = fa.templateId
override def lfIdentifier(
fa: CreateCommand[_],
templateId: TemplateId.RequiredPkg): lf.data.Ref.Identifier =
IdentifierConverters.lfIdentifier(templateId)
}
}
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 hasTemplateId: HasTemplateId[ExerciseCommand] =
new HasTemplateId[ExerciseCommand] {
override def templateId(fa: ExerciseCommand[_]): TemplateId.OptionalPkg = fa.templateId
override def lfIdentifier(
fa: ExerciseCommand[_],
templateId: TemplateId.RequiredPkg): lf.data.Ref.Identifier =
IdentifierConverters.lfIdentifier(templateId, fa.choice)
}
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http.json
import com.digitalasset.daml.lf
import com.digitalasset.daml.lf.value.json.ApiCodecCompressed
import com.digitalasset.http.util.ApiValueToLfValueConverter
import com.digitalasset.ledger.api.{v1 => lav1}
import scalaz.std.list._
import scalaz.syntax.show._
import scalaz.syntax.traverse._
import scalaz.{\/, \/-}
import spray.json.{JsObject, JsValue}
class ApiValueToJsValueConverter(apiToLf: ApiValueToLfValueConverter.ApiValueToLfValue) {
def apiValueToJsValue(a: lav1.value.Value): JsonError \/ JsValue =
apiToLf(a)
.map { b: lf.value.Value[lf.value.Value.AbsoluteContractId] =>
ApiCodecCompressed.apiValueToJsValue(lfValueOfString(b))
}
.leftMap(x => JsonError(x.shows))
@SuppressWarnings(Array("org.wartremover.warts.Any"))
def apiRecordToJsObject(a: lav1.value.Record): JsonError \/ JsObject = {
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)
case Some(v) => apiValueToJsValue(v).map(field.label -> _)
}
private def lfValueOfString(
lfValue: lf.value.Value[lf.value.Value.AbsoluteContractId]): lf.value.Value[String] =
lfValue.mapContractId(x => x.coid)
}

View File

@ -0,0 +1,85 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http.json
import com.digitalasset.daml.lf
import com.digitalasset.http.domain.HasTemplateId
import com.digitalasset.http.{PackageService, domain}
import com.digitalasset.ledger.api.{v1 => lav1}
import scalaz.syntax.show._
import scalaz.syntax.traverse._
import scalaz.{Traverse, \/}
import spray.json.{JsObject, JsValue, JsonReader}
import scala.language.higherKinds
class DomainJsonDecoder(
resolveTemplateId: PackageService.ResolveTemplateId,
jsObjectToApiRecord: (lf.data.Ref.Identifier, JsObject) => JsonError \/ lav1.value.Record,
jsValueToApiValue: (lf.data.Ref.Identifier, JsValue) => JsonError \/ lav1.value.Value) {
def decodeR[F[_]](a: String)(
implicit ev1: JsonReader[F[JsObject]],
ev2: Traverse[F],
ev3: domain.HasTemplateId[F]): JsonError \/ F[lav1.value.Record] =
for {
b <- SprayJson.parse(a).leftMap(e => JsonError(e.shows))
c <- SprayJson.mustBeJsObject(b)
d <- decodeR(c)
} yield d
def decodeR[F[_]](a: JsObject)(
implicit ev1: JsonReader[F[JsObject]],
ev2: Traverse[F],
ev3: domain.HasTemplateId[F]): JsonError \/ F[lav1.value.Record] =
for {
b <- SprayJson.decode[F[JsObject]](a)(ev1).leftMap(e => JsonError(e.shows))
c <- decodeUnderlyingRecords(b)
} yield c
@SuppressWarnings(Array("org.wartremover.warts.Any"))
def decodeUnderlyingRecords[F[_]: Traverse: domain.HasTemplateId](
fa: F[JsObject]): JsonError \/ F[lav1.value.Record] = {
for {
damlLfId <- lookupLfIdentifier(fa)
apiValue <- fa.traverse(jsObject => jsObjectToApiRecord(damlLfId, jsObject))
} yield apiValue
}
def decodeV[F[_]](a: String)(
implicit ev1: JsonReader[F[JsValue]],
ev2: Traverse[F],
ev3: domain.HasTemplateId[F]): JsonError \/ F[lav1.value.Value] =
for {
b <- SprayJson.parse(a).leftMap(e => JsonError(e.shows))
d <- decodeV(b)
} yield d
def decodeV[F[_]](a: JsValue)(
implicit ev1: JsonReader[F[JsValue]],
ev2: Traverse[F],
ev3: domain.HasTemplateId[F]): JsonError \/ F[lav1.value.Value] =
for {
b <- SprayJson.decode[F[JsValue]](a)(ev1).leftMap(e => JsonError(e.shows))
c <- decodeUnderlyingValues(b)
} yield c
@SuppressWarnings(Array("org.wartremover.warts.Any"))
def decodeUnderlyingValues[F[_]: Traverse: domain.HasTemplateId](
fa: F[JsValue]): JsonError \/ F[lav1.value.Value] = {
for {
damlLfId <- lookupLfIdentifier(fa)
apiValue <- fa.traverse(jsValue => jsValueToApiValue(damlLfId, jsValue))
} yield apiValue
}
private def lookupLfIdentifier[F[_]: domain.HasTemplateId](
fa: F[_]): JsonError \/ lf.data.Ref.Identifier = {
val H: HasTemplateId[F] = implicitly
val templateId: domain.TemplateId.OptionalPkg = H.templateId(fa)
resolveTemplateId(templateId)
.map(x => H.lfIdentifier(fa, x))
.leftMap(e => JsonError("lookupLfIdentifier: " + e.shows))
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http.json
import com.digitalasset.ledger.api.{v1 => lav1}
import scalaz.syntax.show._
import scalaz.syntax.traverse._
import scalaz.{Traverse, \/}
import spray.json.{JsObject, JsValue, JsonWriter}
import scala.language.higherKinds
class DomainJsonEncoder(
apiRecordToJsObject: lav1.value.Record => JsonError \/ JsObject,
apiValueToJsValue: lav1.value.Value => JsonError \/ JsValue) {
def encodeR[F[_]](fa: F[lav1.value.Record])(
implicit ev1: Traverse[F],
ev2: JsonWriter[F[JsObject]]): JsonError \/ JsObject =
for {
a <- encodeUnderlyingRecord(fa)
b <- SprayJson.encode[F[JsObject]](a)(ev2).leftMap(e => JsonError(e.shows))
c <- SprayJson.mustBeJsObject(b)
} yield c
// encode underlying values
@SuppressWarnings(Array("org.wartremover.warts.Any"))
def encodeUnderlyingRecord[F[_]: Traverse](fa: F[lav1.value.Record]): JsonError \/ F[JsObject] =
fa.traverse(a => apiRecordToJsObject(a))
def encodeV[F[_]](fa: F[lav1.value.Value])(
implicit ev1: Traverse[F],
ev2: JsonWriter[F[JsValue]]): JsonError \/ JsValue =
for {
a <- encodeUnderlyingValue(fa)
b <- SprayJson.encode[F[JsValue]](a)(ev2).leftMap(e => JsonError(e.shows))
} yield b
// encode underlying values
@SuppressWarnings(Array("org.wartremover.warts.Any"))
def encodeUnderlyingValue[F[_]: Traverse](fa: F[lav1.value.Value]): JsonError \/ F[JsValue] =
fa.traverse(a => apiValueToJsValue(a))
}

View File

@ -0,0 +1,52 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http.json
import com.digitalasset.daml.lf
import com.digitalasset.daml.lf.value.json.ApiCodecCompressed
import com.digitalasset.http.json.JsValueToApiValueConverter.LfTypeLookup
import com.digitalasset.ledger.api.{v1 => lav1}
import com.digitalasset.platform.participant.util.LfEngineToApi
import scalaz.{-\/, \/, \/-}
import spray.json.{JsObject, JsValue}
class JsValueToApiValueConverter(lfTypeLookup: LfTypeLookup) {
def jsValueToApiValue(
lfId: lf.data.Ref.Identifier,
jsValue: JsValue): JsonError \/ lav1.value.Value = {
for {
lfValue <- \/.fromTryCatchNonFatal(
ApiCodecCompressed.jsValueToApiValue(jsValue, lfId, lfTypeLookup))
.leftMap(JsonError.toJsonError)
.map(toAbsoluteContractId)
apiValue <- \/.fromEither(LfEngineToApi.lfValueToApiValue(verbose = true, lfValue))
.leftMap(JsonError.toJsonError)
} yield apiValue
}
def jsObjectToApiRecord(
lfId: lf.data.Ref.Identifier,
jsObject: JsObject): JsonError \/ lav1.value.Record =
for {
a <- jsValueToApiValue(lfId, jsObject)
b <- mustBeApiRecord(a)
} yield b
private def mustBeApiRecord(a: lav1.value.Value): JsonError \/ lav1.value.Record = a.sum match {
case lav1.value.Value.Sum.Record(b) => \/-(b)
case _ => -\/(JsonError(s"Expected ${classOf[lav1.value.Value.Sum.Record]}, got: $a"))
}
private def toAbsoluteContractId(
a: lf.value.Value[String]): lf.value.Value[lf.value.Value.AbsoluteContractId] =
a.mapContractId(cid =>
lf.value.Value.AbsoluteContractId(lf.data.Ref.ContractIdString.assertFromString(cid)))
}
object JsValueToApiValueConverter {
type LfTypeLookup = lf.data.Ref.Identifier => Option[lf.iface.DefDataType.FWT]
}

View File

@ -0,0 +1,21 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http.json
import scalaz.Show
import scalaz.syntax.show._
final case class JsonError(message: String)
object JsonError {
def toJsonError(e: String) = JsonError(e)
def toJsonError[E: Show](e: E) = JsonError(e.shows)
def toJsonError(e: Throwable) = JsonError(e.getMessage)
implicit val ShowInstance: Show[JsonError] = Show shows { f =>
s"JsonError: ${f.message}"
}
}

View File

@ -3,15 +3,46 @@
package com.digitalasset.http.json
import spray.json._
import com.digitalasset.http.domain
import java.time.Instant
import com.digitalasset.http.domain
import com.digitalasset.http.json.TaggedJsonFormat._
import com.digitalasset.ledger.api.refinements.{ApiTypes => lar}
import scalaz.{-\/, \/-}
import spray.json._
object JsonProtocol extends DefaultJsonProtocol {
implicit val LedgerIdFormat: JsonFormat[lar.LedgerId] = taggedJsonFormat[String, lar.LedgerIdTag]
implicit val ApplicationIdFormat: JsonFormat[lar.ApplicationId] =
taggedJsonFormat[String, lar.ApplicationIdTag]
implicit val WorkflowIdFormat: JsonFormat[lar.WorkflowId] =
taggedJsonFormat[String, lar.WorkflowIdTag]
implicit val PartyFormat: JsonFormat[lar.Party] =
taggedJsonFormat[String, lar.PartyTag]
implicit val CommandIdFormat: JsonFormat[lar.CommandId] =
taggedJsonFormat[String, lar.CommandIdTag]
implicit val ChoiceFormat: JsonFormat[lar.Choice] = taggedJsonFormat[String, lar.ChoiceTag]
implicit val ContractIdFormat: JsonFormat[lar.ContractId] =
taggedJsonFormat[String, lar.ContractIdTag]
implicit val JwtPayloadFormat: RootJsonFormat[domain.JwtPayload] = jsonFormat3(domain.JwtPayload)
implicit val InstantFormat: JsonFormat[java.time.Instant] = new JsonFormat[Instant] {
override def write(obj: Instant): JsValue = JsNumber(obj.toEpochMilli)
override def read(json: JsValue): Instant = json match {
case JsNumber(a) => java.time.Instant.ofEpochMilli(a.toLongExact)
case _ => deserializationError("java.time.Instant must be epoch millis")
}
}
implicit def TemplateIdFormat[A: JsonFormat]: RootJsonFormat[domain.TemplateId[A]] =
jsonFormat3(domain.TemplateId.apply[A])
@ -40,11 +71,16 @@ object JsonProtocol extends DefaultJsonProtocol {
implicit val GetActiveContractsRequestFormat: RootJsonFormat[domain.GetActiveContractsRequest] =
jsonFormat1(domain.GetActiveContractsRequest)
// sigh @ induction
implicit def SeqJsonWriter[A: JsonWriter]: JsonWriter[Seq[A]] =
as => JsArray(as.iterator.map(_.toJson).toVector)
implicit val GetActiveContractsResponseFormat
: JsonWriter[domain.GetActiveContractsResponse[JsValue]] =
gacr => JsString(gacr.toString) // TODO actual format
: RootJsonFormat[domain.GetActiveContractsResponse[JsValue]] =
jsonFormat3(domain.GetActiveContractsResponse[JsValue])
implicit val CommandMetaFormat: RootJsonFormat[domain.CommandMeta] = jsonFormat4(
domain.CommandMeta)
implicit val CreateCommandFormat: RootJsonFormat[domain.CreateCommand[JsObject]] = jsonFormat3(
domain.CreateCommand[JsObject])
implicit val ExerciseCommandFormat: RootJsonFormat[domain.ExerciseCommand[JsObject]] =
jsonFormat5(domain.ExerciseCommand[JsObject])
}

View File

@ -14,8 +14,11 @@ private[http] object ResponseFormats {
}
def resultJsObject[A: JsonWriter](a: A): JsObject = {
val result: JsValue = a.toJson
JsObject(statusField(StatusCodes.OK), ("result", result))
resultJsObject(a.toJson)
}
def resultJsObject(a: JsValue): JsObject = {
JsObject(statusField(StatusCodes.OK), ("result", a))
}
def statusField(status: StatusCode): (String, JsNumber) =

View File

@ -0,0 +1,55 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http.json
import scalaz.{-\/, Show, \/, \/-}
import spray.json.{JsValue, JsonReader, _}
@SuppressWarnings(Array("org.wartremover.warts.Any"))
object SprayJson {
sealed abstract class Error extends Product with Serializable
final case class JsonReaderError(value: String, message: String) extends Error
final case class JsonWriterError(value: Any, message: String) extends Error
object Error {
implicit val show: Show[Error] = Show shows {
case a: JsonReaderError => JsonReaderError.ShowInstance.shows(a)
case a: JsonWriterError => JsonWriterError.ShowInstance.shows(a)
}
}
object JsonReaderError {
implicit val ShowInstance: Show[JsonReaderError] = Show shows { f =>
s"JsonReaderError. Cannot read JSON: <${f.value}>. Cause: ${f.message}"
}
}
object JsonWriterError {
implicit val ShowInstance: Show[JsonWriterError] = Show shows { f =>
s"JsonWriterError. Cannot write value as JSON: <${f.value}>. Cause: ${f.message}"
}
}
def parse(str: String): JsonReaderError \/ JsValue =
\/.fromTryCatchNonFatal(JsonParser(str)).leftMap(e => JsonReaderError(str, e.getMessage))
def decode[A: JsonReader](str: String): JsonReaderError \/ A =
for {
jsValue <- parse(str)
a <- decode(jsValue)
} yield a
def decode[A: JsonReader](a: JsValue): JsonReaderError \/ A =
\/.fromTryCatchNonFatal(a.convertTo[A]).leftMap(e => JsonReaderError(a.toString, e.getMessage))
def encode[A: JsonWriter](a: A): JsonWriterError \/ JsValue = {
import spray.json._
\/.fromTryCatchNonFatal(a.toJson).leftMap(e => JsonWriterError(a, e.getMessage))
}
def mustBeJsObject(a: JsValue): JsonError \/ JsObject = a match {
case b: JsObject => \/-(b)
case _ => -\/(JsonError(s"Expected JsObject, got: ${a: JsValue}"))
}
}

View File

@ -0,0 +1,11 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http.json
import scalaz.{@@, Tag}
import spray.json.JsonFormat
object TaggedJsonFormat {
def taggedJsonFormat[A: JsonFormat, T]: JsonFormat[A @@ T] = Tag.subst(implicitly[JsonFormat[A]])
}

View File

@ -0,0 +1,27 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http.util
import com.digitalasset.daml.lf
import com.digitalasset.ledger.api.validation.ValueValidator
import com.digitalasset.ledger.api.{v1 => lav1}
import io.grpc.StatusRuntimeException
import scalaz.{Show, \/}
object ApiValueToLfValueConverter {
final case class Error(cause: StatusRuntimeException)
object Error {
implicit val ErrorShow: Show[Error] = Show shows { e =>
s"ApiValueToLfValueConverter.Error: ${e.cause.getMessage}"
}
}
type ApiValueToLfValue =
lav1.value.Value => Error \/ lf.value.Value[lf.value.Value.AbsoluteContractId]
def apiValueToLfValue: ApiValueToLfValue = { a: lav1.value.Value =>
\/.fromEither(ValueValidator.validateValue(a)).leftMap(e => Error(e))
}
}

View File

@ -8,17 +8,17 @@ import akka.stream.Materializer
import akka.stream.scaladsl.{Sink, Source}
import akka.{Done, NotUsed}
import com.digitalasset.api.util.TimeProvider
import com.digitalasset.api.util.TimestampConversion.fromInstant
import com.digitalasset.http.util.FutureUtil.toFuture
import com.digitalasset.ledger.api.domain.LedgerId
import com.digitalasset.ledger.api.refinements.ApiTypes.{ApplicationId, WorkflowId}
import com.digitalasset.ledger.api.v1.command_submission_service.SubmitRequest
import com.digitalasset.ledger.api.v1.commands.{Command, Commands}
import com.digitalasset.ledger.api.refinements.ApiTypes.{
ApplicationId,
CommandId,
Party,
WorkflowId
}
import com.digitalasset.ledger.api.v1.ledger_offset.LedgerOffset
import com.digitalasset.ledger.api.v1.transaction.Transaction
import com.digitalasset.ledger.api.v1.transaction_filter.{Filters, TransactionFilter}
import com.digitalasset.ledger.client.LedgerClient
import com.google.protobuf.empty.Empty
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
@ -33,9 +33,7 @@ class ClientUtil(
private val ledgerId = client.ledgerId
private val packageClient = client.packageClient
private val commandClient = client.commandClient
private val transactionClient = client.transactionClient
private val activeContractSetClient = client.activeContractSetClient
def listPackages(implicit ec: ExecutionContext): Future[Set[String]] =
packageClient.listPackages().map(_.packageIds.toSet)
@ -43,30 +41,14 @@ class ClientUtil(
def ledgerEnd(implicit ec: ExecutionContext): Future[LedgerOffset] =
transactionClient.getLedgerEnd.flatMap(response => toFuture(response.offset))
def submitCommand(party: String, workflowId: WorkflowId, cmd: Command.Command): Future[Empty] = {
val now = timeProvider.getCurrentTime
val commands = Commands(
ledgerId = LedgerId.unwrap(ledgerId),
workflowId = WorkflowId.unwrap(workflowId),
applicationId = ApplicationId.unwrap(applicationId),
commandId = uniqueId,
party = party,
ledgerEffectiveTime = Some(fromInstant(now)),
maximumRecordTime = Some(fromInstant(now.plusNanos(ttl.toNanos))),
commands = Seq(Command(cmd))
)
commandClient.submitSingleCommand(SubmitRequest(Some(commands), None))
}
def nextTransaction(party: String, offset: LedgerOffset)(
def nextTransaction(party: Party, offset: LedgerOffset)(
implicit mat: Materializer): Future[Transaction] =
transactionClient
.getTransactions(offset, None, transactionFilter(party))
.take(1L)
.runWith(Sink.head)
def subscribe(party: String, offset: LedgerOffset, max: Option[Long])(f: Transaction => Unit)(
def subscribe(party: Party, offset: LedgerOffset, max: Option[Long])(f: Transaction => Unit)(
implicit mat: Materializer): Future[Done] = {
val source: Source[Transaction, NotUsed] =
transactionClient.getTransactions(offset, None, transactionFilter(party))
@ -77,11 +59,13 @@ class ClientUtil(
}
object ClientUtil {
def transactionFilter(parties: String*): TransactionFilter =
TransactionFilter(parties.map((_, Filters.defaultInstance)).toMap)
def transactionFilter(ps: Party*): TransactionFilter =
TransactionFilter(Party.unsubst(ps).map((_, Filters.defaultInstance)).toMap)
def uniqueId: String = UUID.randomUUID.toString
def uniqueId(): String = UUID.randomUUID.toString
def workflowIdFromParty(p: String): WorkflowId =
WorkflowId(s"$p Workflow")
def workflowIdFromParty(p: Party): WorkflowId =
WorkflowId(s"${Party.unwrap(p)} Workflow")
def uniqueCommandId(): CommandId = CommandId(uniqueId())
}

View File

@ -0,0 +1,75 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http.util
import java.time.Instant
import com.digitalasset.api.util.TimestampConversion.fromInstant
import com.digitalasset.ledger.api.refinements.{ApiTypes => lar}
import com.digitalasset.ledger.api.{v1 => lav1}
object Commands {
def create(
templateId: lar.TemplateId,
arguments: lav1.value.Record): lav1.commands.Command.Command.Create =
lav1.commands.Command.Command.Create(
lav1.commands.CreateCommand(
templateId = Some(lar.TemplateId.unwrap(templateId)),
createArguments = Some(arguments)))
def exercise(
templateId: lar.TemplateId,
contractId: lar.ContractId,
choice: lar.Choice,
arguments: lav1.value.Record): lav1.commands.Command.Command.Exercise = {
val choiceStr: String = lar.Choice.unwrap(choice)
val id: lav1.value.Identifier = lar.TemplateId.unwrap(templateId)
lav1.commands.Command.Command.Exercise(
lav1.commands.ExerciseCommand(
templateId = Some(id),
contractId = lar.ContractId.unwrap(contractId),
choice = choiceStr,
choiceArgument = Some(lav1.value.Value(
lav1.value.Value.Sum.Record(recordWithRecordId(arguments, id, choiceStr))))
))
}
private def recordWithRecordId(
record: lav1.value.Record,
templateId: lav1.value.Identifier,
choice: String): lav1.value.Record = {
val recordId = lav1.value.Identifier(
packageId = templateId.packageId,
moduleName = templateId.moduleName,
entityName = choice)
record.copy(recordId = Some(recordId))
}
def submitAndWaitRequest(
ledgerId: lar.LedgerId,
applicationId: lar.ApplicationId,
workflowId: lar.WorkflowId,
commandId: lar.CommandId,
ledgerEffectiveTime: Instant,
maximumRecordTime: Instant,
party: lar.Party,
command: lav1.commands.Command.Command): lav1.command_service.SubmitAndWaitRequest = {
val commands = lav1.commands.Commands(
ledgerId = lar.LedgerId.unwrap(ledgerId),
workflowId = lar.WorkflowId.unwrap(workflowId),
applicationId = lar.ApplicationId.unwrap(applicationId),
commandId = lar.CommandId.unwrap(commandId),
party = lar.Party.unwrap(party),
ledgerEffectiveTime = Some(fromInstant(ledgerEffectiveTime)),
maximumRecordTime = Some(fromInstant(maximumRecordTime)),
commands = Seq(lav1.commands.Command(command))
)
lav1.command_service.SubmitAndWaitRequest(Some(commands))
}
}

View File

@ -4,17 +4,21 @@
package com.digitalasset.http.util
import scalaz.EitherT.rightT
import scalaz.{EitherT, Functor, Show, \/}
import scalaz.syntax.show._
import scalaz.{-\/, EitherT, Functor, Show, \/, \/-}
import scala.concurrent.Future
import scala.concurrent.{ExecutionContext, Future}
import scala.language.higherKinds
import scala.util.Try
object FutureUtil {
def toFuture[A](o: Option[A]): Future[A] =
o.fold(Future.failed[A](new IllegalStateException(s"Empty option: $o")))(a =>
Future.successful(a))
def toFuture[A](a: Try[A]): Future[A] =
a.fold(e => Future.failed(e), a => Future.successful(a))
def toFuture[A: Show, B](a: A \/ B): Future[B] =
a.fold(e => Future.failed(new IllegalStateException(e.shows)), a => Future.successful(a))
@ -22,4 +26,12 @@ object FutureUtil {
final class LiftET[E](private val ignore: Int) extends AnyVal {
def apply[F[_]: Functor, A](fa: F[A]): EitherT[F, E, A] = rightT(fa)
}
def stripLeft[A: Show, B](fa: Future[A \/ B])(implicit ec: ExecutionContext): Future[B] =
fa.flatMap {
case -\/(e) =>
Future.failed(new IllegalStateException(e.shows))
case \/-(a) =>
Future.successful(a)
}
}

View File

@ -0,0 +1,67 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http.util
import com.digitalasset.daml.lf
import com.digitalasset.ledger.api.refinements.{ApiTypes => lar}
import com.digitalasset.ledger.api.{v1 => lav1}
import com.digitalasset.http
object IdentifierConverters {
def lfIdentifier(a: lar.TemplateId): lf.data.Ref.Identifier =
lfIdentifier(lar.TemplateId.unwrap(a))
def lfIdentifier(a: lav1.value.Identifier): lf.data.Ref.Identifier = {
import lf.data.Ref
Ref.Identifier(
Ref.PackageId.assertFromString(a.packageId),
Ref.QualifiedName(
Ref.ModuleName.assertFromString(a.moduleName),
Ref.DottedName.assertFromString(a.entityName))
)
}
def lfIdentifier(a: http.domain.TemplateId.RequiredPkg): lf.data.Ref.Identifier = {
import lf.data.Ref
Ref.Identifier(
Ref.PackageId.assertFromString(a.packageId),
Ref.QualifiedName(
Ref.ModuleName.assertFromString(a.moduleName),
Ref.DottedName.assertFromString(a.entityName))
)
}
def lfIdentifier(
id: http.domain.TemplateId.RequiredPkg,
choice: lar.Choice): lf.data.Ref.Identifier = {
import lf.data.Ref
Ref.Identifier(
Ref.PackageId.assertFromString(id.packageId),
Ref.QualifiedName(
Ref.ModuleName.assertFromString(id.moduleName),
Ref.DottedName.assertFromString(lar.Choice.unwrap(choice)))
)
}
def apiIdentifier(a: lf.data.Ref.Identifier): lav1.value.Identifier =
lav1.value.Identifier(
packageId = a.packageId,
moduleName = a.qualifiedName.module.dottedName,
entityName = a.qualifiedName.name.dottedName)
def apiIdentifier(a: lar.TemplateId): lav1.value.Identifier = lar.TemplateId.unwrap(a)
def apiIdentifier(a: http.domain.TemplateId.RequiredPkg): lav1.value.Identifier =
lav1.value.Identifier(
packageId = a.packageId,
moduleName = a.moduleName,
entityName = a.entityName)
def refApiIdentifier(a: http.domain.TemplateId.RequiredPkg): lar.TemplateId =
lar.TemplateId(apiIdentifier(a))
def apiLedgerId(a: com.digitalasset.ledger.api.domain.LedgerId): lar.LedgerId =
lar.LedgerId(com.digitalasset.ledger.api.domain.LedgerId.unwrap(a))
}

View File

@ -0,0 +1,13 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http.util
import com.digitalasset.daml.lf.data.ImmArray.ImmArraySeq
import com.digitalasset.ledger.api.v1.event.CreatedEvent
import com.digitalasset.ledger.api.v1.transaction.Transaction
object Transactions {
def decodeAllCreatedEvents(transaction: Transaction): ImmArraySeq[CreatedEvent] =
transaction.events.iterator.flatMap(_.event.created.toList).to[ImmArraySeq]
}

View File

@ -0,0 +1,13 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@ -3,18 +3,21 @@
package com.digitalasset.http
import com.digitalasset.ledger.api.{v1 => API}
import com.digitalasset.ledger.api.{v1 => lav1}
import org.scalacheck.Gen
object Generators {
def genApiIdentifier: Gen[API.value.Identifier] =
def genApiIdentifier: Gen[lav1.value.Identifier] =
for {
p <- Gen.identifier
m <- Gen.identifier
e <- Gen.identifier
} yield API.value.Identifier(packageId = p, moduleName = m, entityName = e)
} yield lav1.value.Identifier(packageId = p, moduleName = m, entityName = e)
def genTemplateId[A](implicit ev: PackageIdGen[A]): Gen[domain.TemplateId[A]] =
def genDomainTemplateId: Gen[domain.TemplateId.RequiredPkg] =
genApiIdentifier.map(domain.TemplateId.fromLedgerApi)
def genDomainTemplateIdO[A](implicit ev: PackageIdGen[A]): Gen[domain.TemplateId[A]] =
for {
p <- ev.gen
m <- Gen.identifier
@ -24,12 +27,15 @@ object Generators {
def nonEmptySet[A](gen: Gen[A]): Gen[Set[A]] = Gen.nonEmptyListOf(gen).map(_.toSet)
// Generate Identifiers with unique packageId values, but the same moduleName and entityName.
def genDuplicateApiIdentifiers: Gen[List[API.value.Identifier]] =
def genDuplicateApiIdentifiers: Gen[List[lav1.value.Identifier]] =
for {
id0 <- genApiIdentifier
otherPackageIds <- nonEmptySet(Gen.identifier.filter(x => x != id0.packageId))
} yield id0 :: otherPackageIds.map(a => id0.copy(packageId = a)).toList
def genDuplicateDomainTemplateIdR: Gen[List[domain.TemplateId.RequiredPkg]] =
genDuplicateApiIdentifiers.map(xs => xs.map(domain.TemplateId.fromLedgerApi))
trait PackageIdGen[A] {
def gen: Gen[A]
}

View File

@ -3,4 +3,311 @@
package com.digitalasset.http
class HttpServiceIntegrationTest {}
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.stream.ActorMaterializer
import akka.util.ByteString
import com.digitalasset.daml.bazeltools.BazelRunfiles._
import com.digitalasset.grpc.adapter.{AkkaExecutionSequencerPool, ExecutionSequencerFactory}
import com.digitalasset.http.HttpServiceTestFixture.{jsonCodecs, withHttpService, withLedger}
import com.digitalasset.http.domain.TemplateId.OptionalPkg
import com.digitalasset.http.json._
import com.digitalasset.http.util.TestUtil.requiredFile
import com.digitalasset.ledger.api.refinements.{ApiTypes => lar}
import com.digitalasset.ledger.api.v1.value.Record
import com.digitalasset.ledger.api.v1.{value => v}
import com.typesafe.scalalogging.StrictLogging
import org.scalatest._
import scalaz.\/-
import scalaz.syntax.functor._
import scalaz.syntax.show._
import spray.json._
import scala.concurrent.{ExecutionContext, Future}
@SuppressWarnings(Array("org.wartremover.warts.Any"))
class HttpServiceIntegrationTest
extends AsyncFreeSpec
with Matchers
with Inside
with StrictLogging {
import json.JsonProtocol._
private val dar = requiredFile(rlocation("docs/quickstart-model.dar"))
.fold(e => throw new IllegalStateException(e), identity)
private val testId: String = this.getClass.getSimpleName
implicit val asys: ActorSystem = ActorSystem(testId)
implicit val mat: ActorMaterializer = ActorMaterializer()
implicit val aesf: ExecutionSequencerFactory = new AkkaExecutionSequencerPool(testId)(asys)
implicit val ec: ExecutionContext = asys.dispatcher
"contracts/search test" in withHttpService(dar, testId) { (uri: Uri, _, _) =>
Http().singleRequest(HttpRequest(uri = uri.withPath(Uri.Path("/contracts/search")))).flatMap {
resp =>
resp.status shouldBe StatusCodes.OK
val bodyF: Future[String] = getResponseDataBytes(resp, debug = true)
bodyF.flatMap { body =>
val jsonAst: JsValue = body.parseJson
inside(jsonAst) {
case JsObject(fields) =>
inside(fields.get("status")) {
case Some(JsNumber(status)) => status shouldBe BigDecimal("200")
}
inside(fields.get("result")) {
case Some(JsArray(result)) => result.length should be > 0
}
}
}
}: Future[Assertion]
}
"command/create IOU" in withHttpService(dar, testId) { (uri, encoder, decoder) =>
val command: domain.CreateCommand[v.Record] = iouCreateCommand
val input: JsObject = encoder.encodeR(command).valueOr(e => fail(e.shows))
postJson(uri.withPath(Uri.Path("/command/create")), input).flatMap {
case (status, output) =>
status shouldBe StatusCodes.OK
assertStatus(output, StatusCodes.OK)
inside(output) {
case JsObject(fields) =>
inside(fields.get("result")) {
case Some(activeContract: JsObject) =>
assertActiveContract(decoder, activeContract, command)
}
}
}: Future[Assertion]
}
"command/create IOU with unsupported templateId should return proper error" in withHttpService(
dar,
testId) { (uri, encoder, _) =>
val command: domain.CreateCommand[v.Record] =
iouCreateCommand.copy(templateId = domain.TemplateId(None, "Iou", "Dummy"))
val input: JsObject = encoder.encodeR(command).valueOr(e => fail(e.shows))
postJson(uri.withPath(Uri.Path("/command/create")), input).flatMap {
case (status, output) =>
status shouldBe StatusCodes.OK
assertStatus(output, StatusCodes.BadRequest)
inside(output) {
case JsObject(fields) =>
inside(fields.get("errors")) {
case Some(JsArray(Vector(JsString(errorMsg)))) =>
val unknownTemplateId: domain.TemplateId.NoPkg = domain.TemplateId(
(),
command.templateId.moduleName,
command.templateId.entityName)
errorMsg should include(
s"Cannot resolve ${unknownTemplateId: domain.TemplateId.NoPkg}")
}
}
}: Future[Assertion]
}
"command/exercise IOU_Transfer" in withHttpService(dar, testId) { (uri, encoder, decoder) =>
val create: domain.CreateCommand[v.Record] = iouCreateCommand
val createJson: JsObject = encoder.encodeR(create).valueOr(e => fail(e.shows))
postJson(uri.withPath(Uri.Path("/command/create")), createJson).flatMap {
case (createStatus, createOutput) =>
createStatus shouldBe StatusCodes.OK
assertStatus(createOutput, StatusCodes.OK)
val contractId = getContractId(createOutput)
val exercise: domain.ExerciseCommand[v.Record] = iouExerciseTransferCommand(contractId)
val exerciseJson: JsObject = encoder.encodeR(exercise).valueOr(e => fail(e.shows))
postJson(uri.withPath(Uri.Path("/command/exercise")), exerciseJson).flatMap {
case (exerciseStatus, exerciseOutput) =>
exerciseStatus shouldBe StatusCodes.OK
assertStatus(exerciseOutput, StatusCodes.OK)
println(s"----- exerciseOutput: $exerciseOutput")
inside(exerciseOutput) {
case JsObject(fields) =>
inside(fields.get("result")) {
case Some(JsArray(Vector(activeContract: JsObject))) =>
assertActiveContract(decoder, activeContract, create, exercise)
}
}
}
}: Future[Assertion]
}
"command/exercise IOU_Transfer with unknown contractId should return proper error" in withHttpService(
dar,
testId) { (uri, encoder, _) =>
val contractId = lar.ContractId("NonExistentContractId")
val exercise: domain.ExerciseCommand[v.Record] = iouExerciseTransferCommand(contractId)
val exerciseJson: JsObject = encoder.encodeR(exercise).valueOr(e => fail(e.shows))
postJson(uri.withPath(Uri.Path("/command/exercise")), exerciseJson).flatMap {
case (status, output) =>
status shouldBe StatusCodes.OK
assertStatus(output, StatusCodes.InternalServerError)
inside(output) {
case JsObject(fields) =>
inside(fields.get("errors")) {
case Some(JsArray(Vector(JsString(errorMsg)))) =>
errorMsg should include(
"couldn't find contract AbsoluteContractId(NonExistentContractId)")
}
}
}: Future[Assertion]
}
private def assertActiveContract(
decoder: DomainJsonDecoder,
jsObject: JsObject,
create: domain.CreateCommand[v.Record],
exercise: domain.ExerciseCommand[v.Record]): Assertion = {
// TODO(Leo): check the jsObject.argument is the same as createCommand.argument
println(s"------- jsObject: $jsObject")
println(s"------- create: $create")
println(s"------- exercise: $exercise")
inside(jsObject.fields.get("argument")) {
case Some(JsObject(fields)) =>
fields.size shouldBe (exercise.argument.fields.size + 1) // +1 for the original "iou" from create
}
}
private def assertActiveContract(
decoder: DomainJsonDecoder,
jsObject: JsObject,
command: domain.CreateCommand[v.Record]): Assertion = {
inside(decoder.decodeV[domain.ActiveContract](jsObject)) {
case \/-(activeContract) =>
inside(activeContract.argument.sum.record) {
case Some(argument) => removeRecordId(argument) shouldBe command.argument
}
}
}
"should be able to serialize and deserialize domain commands" in withLedger(dar, testId) {
client =>
jsonCodecs(client).map {
case (encoder, decoder) =>
testCreateCommandEncodingDecoding(encoder, decoder)
testExerciseCommandEncodingDecoding(encoder, decoder)
}: Future[Assertion]
}
private def testCreateCommandEncodingDecoding(
encoder: DomainJsonEncoder,
decoder: DomainJsonDecoder): Assertion = {
import json.JsonProtocol._
val command0: domain.CreateCommand[v.Record] = iouCreateCommand
val x = for {
jsonObj <- encoder.encodeR(command0)
command1 <- decoder.decodeR[domain.CreateCommand](jsonObj)
} yield command1.map(removeRecordId) should ===(command0)
x.fold(e => fail(e.shows), identity)
}
private def testExerciseCommandEncodingDecoding(
encoder: DomainJsonEncoder,
decoder: DomainJsonDecoder): Assertion = {
import json.JsonProtocol._
val command0: domain.ExerciseCommand[v.Record] = iouExerciseTransferCommand(
lar.ContractId("a-contract-ID"))
val x = for {
jsonObj <- encoder.encodeR(command0)
command1 <- decoder.decodeR[domain.ExerciseCommand](jsonObj)
} yield command1.map(removeRecordId) should ===(command0)
x.fold(e => fail(e.shows), identity)
}
"request non-existent endpoint should return 404 with no data" in withHttpService(dar, testId) {
(uri: Uri, _, _) =>
Http()
.singleRequest(HttpRequest(uri = uri.withPath(Uri.Path("/contracts/does-not-exist"))))
.flatMap { resp =>
resp.status shouldBe StatusCodes.NotFound
val bodyF: Future[String] = getResponseDataBytes(resp)
bodyF.flatMap { body =>
body should have length 0
}
}: Future[Assertion]
}
private def getResponseDataBytes(resp: HttpResponse, debug: Boolean = false): Future[String] = {
val fb = resp.entity.dataBytes.runFold(ByteString.empty)((b, a) => b ++ a).map(_.utf8String)
if (debug) fb.foreach(x => logger.info(s"---- response data: $x"))
fb
}
private def removeRecordId(a: v.Record): v.Record = a.copy(recordId = None)
private def iouCreateCommand: domain.CreateCommand[v.Record] = {
val templateId: OptionalPkg = domain.TemplateId(None, "Iou", "Iou")
val arg: Record = v.Record(
fields = List(
v.RecordField("issuer", Some(v.Value(v.Value.Sum.Party("Alice")))),
v.RecordField("owner", Some(v.Value(v.Value.Sum.Party("Alice")))),
v.RecordField("currency", Some(v.Value(v.Value.Sum.Text("USD")))),
v.RecordField("amount", Some(v.Value(v.Value.Sum.Decimal("999.99")))),
v.RecordField("observers", Some(v.Value(v.Value.Sum.List(v.List()))))
))
domain.CreateCommand(templateId, arg, None)
}
private def iouExerciseTransferCommand(
contractId: lar.ContractId): domain.ExerciseCommand[v.Record] = {
val templateId: OptionalPkg = domain.TemplateId(None, "Iou", "Iou")
val arg: Record = v.Record(
fields = List(v.RecordField("newOwner", Some(v.Value(v.Value.Sum.Party("Alice")))))
)
val choice = lar.Choice("Iou_Transfer")
domain.ExerciseCommand(templateId, contractId, choice, arg, None)
}
private def postJson(uri: Uri, json: JsValue): Future[(StatusCode, JsValue)] = {
logger.info(s"postJson: $uri json: $json")
Http()
.singleRequest(
HttpRequest(
method = HttpMethods.POST,
uri = uri,
entity = HttpEntity(ContentTypes.`application/json`, json.prettyPrint))
)
.flatMap { resp =>
val bodyF: Future[String] = getResponseDataBytes(resp, debug = true)
bodyF.map(body => (resp.status, body.parseJson))
}
}
private def assertStatus(jsObj: JsValue, expectedStatus: StatusCode): Assertion = {
inside(jsObj) {
case JsObject(fields) =>
inside(fields.get("status")) {
case Some(JsNumber(status)) => status shouldBe BigDecimal(expectedStatus.intValue)
}
}
}
private def getContractId(output: JsValue): lar.ContractId =
inside(output) {
case JsObject(topFields) =>
inside(topFields.get("result")) {
case Some(JsObject(fields)) =>
inside(fields.get("contractId")) {
case Some(JsString(contractId)) => lar.ContractId(contractId)
}
}
}
}

View File

@ -0,0 +1,147 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http
import java.io.File
import akka.actor.ActorSystem
import akka.http.scaladsl.Http.ServerBinding
import akka.http.scaladsl.model.Uri
import akka.stream.Materializer
import com.digitalasset.grpc.adapter.ExecutionSequencerFactory
import com.digitalasset.http.json.{DomainJsonDecoder, DomainJsonEncoder}
import com.digitalasset.http.util.FutureUtil
import com.digitalasset.http.util.FutureUtil.toFuture
import com.digitalasset.http.util.IdentifierConverters.apiLedgerId
import com.digitalasset.http.util.TestUtil.findOpenPort
import com.digitalasset.ledger.api.domain.LedgerId
import com.digitalasset.ledger.api.refinements.ApiTypes.ApplicationId
import com.digitalasset.ledger.client.LedgerClient
import com.digitalasset.ledger.client.configuration.{
CommandClientConfiguration,
LedgerClientConfiguration,
LedgerIdRequirement
}
import com.digitalasset.ledger.service.LedgerReader
import com.digitalasset.platform.common.LedgerIdMode
import com.digitalasset.platform.sandbox.SandboxServer
import com.digitalasset.platform.sandbox.config.SandboxConfig
import com.digitalasset.platform.services.time.TimeProviderType
import scalaz._
import scala.concurrent.{ExecutionContext, Future}
object HttpServiceTestFixture {
def withHttpService[A](dar: File, testName: String)(
testFn: (Uri, DomainJsonEncoder, DomainJsonDecoder) => Future[A])(
implicit asys: ActorSystem,
mat: Materializer,
aesf: ExecutionSequencerFactory,
ec: ExecutionContext): Future[A] = {
val ledgerId = LedgerId(testName)
val applicationId = ApplicationId(testName)
val ledgerF: Future[(SandboxServer, Int)] = for {
port <- toFuture(findOpenPort())
ledger <- Future(SandboxServer(ledgerConfig(port, dar, ledgerId)))
} yield (ledger, port)
val httpServiceF: Future[(ServerBinding, Int)] = for {
(_, ledgerPort) <- ledgerF
httpPort <- toFuture(findOpenPort())
httpService <- stripLeft(HttpService.start("localhost", ledgerPort, applicationId, httpPort))
} yield (httpService, httpPort)
val clientF: Future[LedgerClient] = for {
(_, ledgerPort) <- ledgerF
client <- LedgerClient.singleHost("localhost", ledgerPort, clientConfig(applicationId))
} yield client
val codecsF: Future[(DomainJsonEncoder, DomainJsonDecoder)] = for {
client <- clientF
codecs <- jsonCodecs(client)
} yield codecs
val fa: Future[A] = for {
(_, httpPort) <- httpServiceF
(encoder, decoder) <- codecsF
uri = Uri.from(scheme = "http", host = "localhost", port = httpPort)
a <- testFn(uri, encoder, decoder)
} yield a
fa.onComplete { _ =>
ledgerF.foreach(_._1.close())
httpServiceF.foreach(_._1.unbind())
}
fa
}
def withLedger[A](dar: File, testName: String)(testFn: LedgerClient => Future[A])(
implicit aesf: ExecutionSequencerFactory,
ec: ExecutionContext): Future[A] = {
val ledgerId = LedgerId(testName)
val applicationId = ApplicationId(testName)
val ledgerF: Future[(SandboxServer, Int)] = for {
port <- toFuture(findOpenPort())
ledger <- Future(SandboxServer(ledgerConfig(port, dar, ledgerId)))
} yield (ledger, port)
val clientF: Future[LedgerClient] = for {
(_, ledgerPort) <- ledgerF
client <- LedgerClient.singleHost("localhost", ledgerPort, clientConfig(applicationId))
} yield client
val fa: Future[A] = for {
client <- clientF
a <- testFn(client)
} yield a
fa.onComplete { _ =>
ledgerF.foreach(_._1.close())
}
fa
}
private def ledgerConfig(ledgerPort: Int, dar: File, ledgerId: LedgerId): SandboxConfig =
SandboxConfig.default.copy(
port = ledgerPort,
damlPackages = List(dar),
timeProviderType = TimeProviderType.WallClock,
ledgerIdMode = LedgerIdMode.Static(ledgerId),
)
private def clientConfig[A](applicationId: ApplicationId): LedgerClientConfiguration =
LedgerClientConfiguration(
applicationId = ApplicationId.unwrap(applicationId),
ledgerIdRequirement = LedgerIdRequirement("", enabled = false),
commandClient = CommandClientConfiguration.default,
sslContext = None
)
def jsonCodecs(client: LedgerClient)(
implicit ec: ExecutionContext): Future[(DomainJsonEncoder, DomainJsonDecoder)] = {
import scalaz.std.string._
val ledgerId = apiLedgerId(client.ledgerId)
for {
packageStore <- FutureUtil.stripLeft(LedgerReader.createPackageStore(client.packageClient))
templateIdMap = PackageService.getTemplateIdMap(packageStore)
codecs = HttpService.buildJsonCodecs(ledgerId, packageStore, templateIdMap)
} yield codecs
}
private def stripLeft(fa: Future[HttpService.Error \/ ServerBinding])(
implicit ec: ExecutionContext): Future[ServerBinding] =
fa.flatMap {
case -\/(e) =>
Future.failed(new IllegalStateException(s"Cannot start HTTP Service: ${e.message}"))
case \/-(a) =>
Future.successful(a)
}
}

View File

@ -3,17 +3,19 @@
package com.digitalasset.http
import com.digitalasset.http.Generators.{genApiIdentifier, genDuplicateApiIdentifiers, nonEmptySet}
import com.digitalasset.http.Generators.{
genDomainTemplateId,
genDuplicateDomainTemplateIdR,
nonEmptySet
}
import com.digitalasset.http.PackageService.TemplateIdMap
import com.digitalasset.ledger.api.v1.value.Identifier
import com.digitalasset.ledger.api.{v1 => lav1}
import org.scalacheck.Gen.nonEmptyListOf
import org.scalacheck.Shrink
import org.scalatest.prop.GeneratorDrivenPropertyChecks
import org.scalatest.{FreeSpec, Inside, Matchers}
import scalaz.{-\/, \/-}
import scala.collection.breakOut
class PackageServiceTest
extends FreeSpec
with Matchers
@ -27,62 +29,58 @@ class PackageServiceTest
"PackageService.buildTemplateIdMap" - {
"identifiers with the same (moduleName, entityName) are not unique" in
forAll(genDuplicateApiIdentifiers) { ids =>
forAll(genDuplicateDomainTemplateIdR) { ids =>
val map = PackageService.buildTemplateIdMap(ids.toSet)
map.all shouldBe expectedAll(ids)
map.all shouldBe ids.toSet
map.unique shouldBe Map.empty
}
"2 identifiers with the same (moduleName, entityName) are not unique" in
forAll(genApiIdentifier) { id0 =>
val id1 = id0.copy(packageId = id0.packageId + "aaaa")
forAll(genDomainTemplateId) { id0 =>
val id1 = appendToPackageId("aaaa")(id0)
val map = PackageService.buildTemplateIdMap(Set(id0, id1))
map.all shouldBe expectedAll(List(id0, id1))
map.all shouldBe Set(id0, id1)
map.unique shouldBe Map.empty
}
"pass one specific test case that was failing" in {
val id0 = Identifier("a", "f4", "x")
val id1 = Identifier("b", "f4", "x")
val id0 = domain.TemplateId.fromLedgerApi(lav1.value.Identifier("a", "f4", "x"))
val id1 = domain.TemplateId.fromLedgerApi(lav1.value.Identifier("b", "f4", "x"))
val map = PackageService.buildTemplateIdMap(Set(id0, id1))
map.all shouldBe expectedAll(List(id0, id1))
map.all shouldBe Set(id0, id1)
map.unique shouldBe Map.empty
}
"3 identifiers with the same (moduleName, entityName) are not unique" in
forAll(genApiIdentifier) { id0 =>
val id1 = id0.copy(packageId = id0.packageId + "aaaa")
val id2 = id0.copy(packageId = id0.packageId + "bbbb")
forAll(genDomainTemplateId) { id0 =>
val id1 = appendToPackageId("aaaa")(id0)
val id2 = appendToPackageId("bbbb")(id1)
val map = PackageService.buildTemplateIdMap(Set(id0, id1, id2))
map.all shouldBe expectedAll(List(id0, id1, id2))
map.all shouldBe Set(id0, id1, id2)
map.unique shouldBe Map.empty
}
"TemplateIdMap.all should contain dups and unique identifiers" in
forAll(nonEmptyListOf(Generators.genApiIdentifier), genDuplicateApiIdentifiers) {
(xs, dups) =>
forAll(nonEmptyListOf(genDomainTemplateId), genDuplicateDomainTemplateIdR) { (xs, dups) =>
val map = PackageService.buildTemplateIdMap((xs ++ dups).toSet)
map.all.size should be >= dups.size
map.all.size should be >= map.unique.size
map.all should ===(xs.toSet ++ dups.toSet)
dups.foreach { x =>
map.all.get(PackageService.key3(x)) shouldBe Some(x)
map.all.contains(x) shouldBe true
}
xs.foreach { x =>
map.all.get(PackageService.key3(x)) shouldBe Some(x)
map.all.contains(x) shouldBe true
}
}
"TemplateIdMap.unique should not contain dups" in
forAll(nonEmptyListOf(Generators.genApiIdentifier), genDuplicateApiIdentifiers) {
(xs, dups) =>
forAll(nonEmptyListOf(genDomainTemplateId), genDuplicateDomainTemplateIdR) { (xs, dups) =>
val map = PackageService.buildTemplateIdMap((xs ++ dups).toSet)
map.all.size should be >= dups.size
map.all.size should be >= map.unique.size
map.all should ===(dups.toSet ++ xs.toSet)
xs.foreach { x =>
map.unique.get(PackageService.key2(PackageService.key3(x))) shouldBe Some(x)
map.unique.get(PackageService.key2(x)) shouldBe Some(x)
}
dups.foreach { x =>
map.unique.get(PackageService.key2(PackageService.key3(x))) shouldBe None
map.unique.get(PackageService.key2(x)) shouldBe None
}
}
}
@ -90,11 +88,12 @@ class PackageServiceTest
"PackageService.resolveTemplateIds" - {
"should return all API Identifier by (moduleName, entityName)" in forAll(
nonEmptySet(genApiIdentifier)) { ids =>
nonEmptySet(genDomainTemplateId)) { ids =>
val map = PackageService.buildTemplateIdMap(ids)
val uniqueIds: Set[Identifier] = map.unique.values.toSet
val uniqueDomainIds: Set[domain.TemplateId.OptionalPkg] = uniqueIds.map(x =>
domain.TemplateId(packageId = None, moduleName = x.moduleName, entityName = x.entityName))
val uniqueIds: Set[domain.TemplateId.RequiredPkg] = map.unique.values.toSet
val uniqueDomainIds: Set[domain.TemplateId.OptionalPkg] = uniqueIds.map { x =>
domain.TemplateId(packageId = None, moduleName = x.moduleName, entityName = x.entityName)
}
inside(PackageService.resolveTemplateIds(map)(uniqueDomainIds)) {
case \/-(actualIds) => actualIds.toSet shouldBe uniqueIds
@ -102,10 +101,12 @@ class PackageServiceTest
}
"should return all API Identifier by (packageId, moduleName, entityName)" in forAll(
nonEmptySet(genApiIdentifier)) { ids =>
nonEmptySet(genDomainTemplateId)) { ids =>
val map = PackageService.buildTemplateIdMap(ids)
val domainIds: Set[domain.TemplateId.OptionalPkg] =
ids.map(x => domain.TemplateId(Some(x.packageId), x.moduleName, x.entityName))
ids.map { x =>
domain.TemplateId(Some(x.packageId), x.moduleName, x.entityName)
}
inside(PackageService.resolveTemplateIds(map)(domainIds)) {
case \/-(actualIds) => actualIds.toSet shouldBe ids
@ -113,8 +114,9 @@ class PackageServiceTest
}
"should return error for unmapped Template ID" in forAll(
Generators.genTemplateId[Option[String]]) { templateId: domain.TemplateId.OptionalPkg =>
val map = TemplateIdMap(Map.empty, Map.empty)
Generators.genDomainTemplateIdO[Option[String]]) {
templateId: domain.TemplateId.OptionalPkg =>
val map = TemplateIdMap(Set.empty, Map.empty)
inside(PackageService.resolveTemplateId(map)(templateId)) {
case -\/(e) =>
val templateIdStr: String = templateId.packageId.fold(
@ -125,6 +127,7 @@ class PackageServiceTest
}
}
private def expectedAll(ids: List[Identifier]): Map[domain.TemplateId.RequiredPkg, Identifier] =
ids.map(v => PackageService.key3(v) -> v)(breakOut)
private def appendToPackageId(x: String)(a: domain.TemplateId.RequiredPkg) =
a.copy(packageId = a.packageId + x)
}

View File

@ -0,0 +1,25 @@
// Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http.util
import java.io.File
import java.net.ServerSocket
import scala.util.{Failure, Success, Try}
object TestUtil {
def findOpenPort(): Try[Int] = Try {
val socket = new ServerSocket(0)
val result = socket.getLocalPort
socket.close()
result
}
def requiredFile(fileName: String): Try[File] = {
val file = new File(fileName)
if (file.exists()) Success(file.getAbsoluteFile)
else
Failure(new IllegalStateException(s"File doest not exist: $fileName"))
}
}

View File

@ -33,21 +33,23 @@ class ApiCodecCompressedSpec extends WordSpec with Matchers with GeneratorDriven
}
type Cid = String
private val genCid = Gen.alphaStr.filter(_.nonEmpty)
private val genCid = Gen.zip(Gen.alphaChar, Gen.alphaStr) map { case (h, t) => h +: t }
"API compressed JSON codec" when {
"serializing and parsing a value" should {
"work for arbitrary reference-free types" in forAll(genTypeAndValue(genCid)) {
"work for arbitrary reference-free types" in forAll(
genTypeAndValue(genCid),
minSuccessful(100)) {
case (typ, value) =>
serializeAndParse(value, typ) shouldBe Success(value)
}
"work for many, many values in raw format" in forAll(genAddend) { va =>
"work for many, many values in raw format" in forAll(genAddend, minSuccessful(100)) { va =>
import va.injshrink
implicit val arbInj: Arbitrary[va.Inj[Cid]] = Arbitrary(va.injgen(genCid))
forAll { v: va.Inj[Cid] =>
forAll(minSuccessful(20)) { v: va.Inj[Cid] =>
va.prj(
ApiCodecCompressed.jsValueToApiValue(
ApiCodecCompressed.apiValueToJsValue(va.inj(v)),

View File

@ -6,9 +6,9 @@ package com.digitalasset.ledger.service
import java.io.File
import java.nio.file.Files
import com.digitalasset.daml.lf.data.Ref.PackageId
import com.digitalasset.daml.lf.data.Ref.{PackageId, Identifier}
import com.digitalasset.daml.lf.iface.reader.InterfaceReader
import com.digitalasset.daml.lf.iface.Interface
import com.digitalasset.daml.lf.iface.{Interface, DefDataType}
import com.digitalasset.daml.lf.archive.Reader
import com.digitalasset.daml_lf.DamlLf
import com.digitalasset.daml_lf.DamlLf.Archive
@ -64,4 +64,10 @@ object LedgerReader {
else \/.right(out)
}.leftMap(_.getLocalizedMessage).join
}
def damlLfTypeLookup(packageStore: PackageStore)(id: Identifier): Option[DefDataType.FWT] =
for {
iface <- packageStore.get(id.packageId.toString)
ifaceType <- iface.typeDecls.get(id.qualifiedName)
} yield ifaceType.`type`
}

View File

@ -14,4 +14,19 @@ class SynchronousCommandClient(commandService: CommandService) {
def submitAndWait(submitAndWaitRequest: SubmitAndWaitRequest): Future[Empty] = {
commandService.submitAndWait(submitAndWaitRequest)
}
def submitAndWaitForTransactionId(
submitAndWaitRequest: SubmitAndWaitRequest): Future[SubmitAndWaitForTransactionIdResponse] = {
commandService.submitAndWaitForTransactionId(submitAndWaitRequest)
}
def submitAndWaitForTransaction(
submitAndWaitRequest: SubmitAndWaitRequest): Future[SubmitAndWaitForTransactionResponse] = {
commandService.submitAndWaitForTransaction(submitAndWaitRequest)
}
def submitAndWaitForTransactionTree(submitAndWaitRequest: SubmitAndWaitRequest)
: Future[SubmitAndWaitForTransactionTreeResponse] = {
commandService.submitAndWaitForTransactionTree(submitAndWaitRequest)
}
}