mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
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:
parent
0ffe5945b8
commit
b940951a76
@ -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"))
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
)
|
||||
|
133
ledger-service/http-json/README.md
Normal file
133
ledger-service/http-json/README.md
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
@ -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]
|
||||
}
|
||||
|
@ -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)))
|
||||
)
|
||||
|
||||
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) =>
|
||||
httpResponse(
|
||||
contractsService.search(jwtPayload, a).map(x => format(resultJsObject(x.toString)))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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))))))
|
||||
}
|
||||
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)
|
||||
)
|
||||
|
||||
private def httpResponse(status: StatusCode, output: ByteString): HttpResponse =
|
||||
HttpResponse(
|
||||
status = status,
|
||||
entity = HttpEntity.Strict(ContentTypes.`application/json`, output))
|
||||
case req @ HttpRequest(POST, Uri.Path("/contracts/search"), _, _, _) =>
|
||||
httpResponse(
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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]
|
||||
}
|
@ -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,38 +72,149 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
object GetActiveContractsResponse {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
@ -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]
|
||||
}
|
@ -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}"
|
||||
}
|
||||
}
|
@ -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])
|
||||
}
|
||||
|
@ -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) =
|
||||
|
@ -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}"))
|
||||
}
|
||||
}
|
@ -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]])
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
@ -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]
|
||||
}
|
13
ledger-service/http-json/src/test/resources/logback.xml
Normal file
13
ledger-service/http-json/src/test/resources/logback.xml
Normal 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>
|
@ -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]
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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,74 +29,71 @@ 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) =>
|
||||
val map = PackageService.buildTemplateIdMap((xs ++ dups).toSet)
|
||||
map.all.size should be >= dups.size
|
||||
map.all.size should be >= map.unique.size
|
||||
dups.foreach { x =>
|
||||
map.all.get(PackageService.key3(x)) shouldBe Some(x)
|
||||
}
|
||||
xs.foreach { x =>
|
||||
map.all.get(PackageService.key3(x)) shouldBe Some(x)
|
||||
}
|
||||
forAll(nonEmptyListOf(genDomainTemplateId), genDuplicateDomainTemplateIdR) { (xs, dups) =>
|
||||
val map = PackageService.buildTemplateIdMap((xs ++ dups).toSet)
|
||||
map.all should ===(xs.toSet ++ dups.toSet)
|
||||
dups.foreach { x =>
|
||||
map.all.contains(x) shouldBe true
|
||||
}
|
||||
xs.foreach { x =>
|
||||
map.all.contains(x) shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
"TemplateIdMap.unique should not contain dups" in
|
||||
forAll(nonEmptyListOf(Generators.genApiIdentifier), genDuplicateApiIdentifiers) {
|
||||
(xs, dups) =>
|
||||
val map = PackageService.buildTemplateIdMap((xs ++ dups).toSet)
|
||||
map.all.size should be >= dups.size
|
||||
map.all.size should be >= map.unique.size
|
||||
xs.foreach { x =>
|
||||
map.unique.get(PackageService.key2(PackageService.key3(x))) shouldBe Some(x)
|
||||
}
|
||||
dups.foreach { x =>
|
||||
map.unique.get(PackageService.key2(PackageService.key3(x))) shouldBe None
|
||||
}
|
||||
forAll(nonEmptyListOf(genDomainTemplateId), genDuplicateDomainTemplateIdR) { (xs, dups) =>
|
||||
val map = PackageService.buildTemplateIdMap((xs ++ dups).toSet)
|
||||
map.all should ===(dups.toSet ++ xs.toSet)
|
||||
xs.foreach { x =>
|
||||
map.unique.get(PackageService.key2(x)) shouldBe Some(x)
|
||||
}
|
||||
dups.foreach { x =>
|
||||
map.unique.get(PackageService.key2(x)) shouldBe None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"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,18 +114,20 @@ 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)
|
||||
inside(PackageService.resolveTemplateId(map)(templateId)) {
|
||||
case -\/(e) =>
|
||||
val templateIdStr: String = templateId.packageId.fold(
|
||||
domain.TemplateId((), templateId.moduleName, templateId.entityName).toString)(p =>
|
||||
domain.TemplateId(p, templateId.moduleName, templateId.entityName).toString)
|
||||
e shouldBe PackageService.InputError(s"Cannot resolve $templateIdStr")
|
||||
}
|
||||
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(
|
||||
domain.TemplateId((), templateId.moduleName, templateId.entityName).toString)(p =>
|
||||
domain.TemplateId(p, templateId.moduleName, templateId.entityName).toString)
|
||||
e shouldBe PackageService.InputError(s"Cannot resolve $templateIdStr")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
|
@ -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"))
|
||||
}
|
||||
}
|
@ -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)),
|
||||
|
@ -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`
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user