mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
Return archived events from /command/exercise (#3036)
* Adding `domain.ArchivedContract` * Adding `domain.Contract` * Returning archived and active contracts from `/command/exercise` Improving integration tests, asserting values in the response JSON * Updating documentation * Do not populate workflowId if it is not provided set it to empty string (default), it is optional in the Ledger API
This commit is contained in:
parent
bb309794db
commit
1fa170366d
@ -135,10 +135,14 @@ output, each contract formatted according to :doc:`lf-value-specification`::
|
||||
"status": 200,
|
||||
"result": [
|
||||
{
|
||||
"observers": [],
|
||||
"agreementText": "",
|
||||
"contractId": "#237:0",
|
||||
"signatories": [
|
||||
"Alice"
|
||||
],
|
||||
"contractId": "#489:0",
|
||||
"templateId": {
|
||||
"packageId": "cde2c40565fd8962eaebae7584ae89ba12d301d4c683189dccbbf0d0d67afc05",
|
||||
"packageId": "ac3a64908d9f6b4453329b3d7d8ddea44c83f4f5469de5f7ae19158c69bf8473",
|
||||
"moduleName": "Iou",
|
||||
"entityName": "Iou"
|
||||
},
|
||||
@ -183,10 +187,14 @@ output::
|
||||
{
|
||||
"status": 200,
|
||||
"result": {
|
||||
"observers": [],
|
||||
"agreementText": "",
|
||||
"contractId": "#237:0",
|
||||
"signatories": [
|
||||
"Alice"
|
||||
],
|
||||
"contractId": "#56:0",
|
||||
"templateId": {
|
||||
"packageId": "cde2c40565fd8962eaebae7584ae89ba12d301d4c683189dccbbf0d0d67afc05",
|
||||
"packageId": "ac3a64908d9f6b4453329b3d7d8ddea44c83f4f5469de5f7ae19158c69bf8473",
|
||||
"moduleName": "Iou",
|
||||
"entityName": "Iou"
|
||||
},
|
||||
@ -209,7 +217,7 @@ POST http://localhost:44279/command/exercise
|
||||
|
||||
Exercise a choice on a contract.
|
||||
|
||||
``"contractId": "#237:0"`` is the value from the create output
|
||||
``"contractId": "#56:0"`` is the value from the create output
|
||||
application/json body::
|
||||
|
||||
{
|
||||
@ -217,7 +225,7 @@ application/json body::
|
||||
"moduleName": "Iou",
|
||||
"entityName": "Iou"
|
||||
},
|
||||
"contractId": "#237:0",
|
||||
"contractId": "#56:0",
|
||||
"choice": "Iou_Transfer",
|
||||
"argument": {
|
||||
"newOwner": "Alice"
|
||||
@ -230,27 +238,47 @@ output::
|
||||
"status": 200,
|
||||
"result": [
|
||||
{
|
||||
"agreementText": "",
|
||||
"contractId": "#441:1",
|
||||
"templateId": {
|
||||
"packageId": "cde2c40565fd8962eaebae7584ae89ba12d301d4c683189dccbbf0d0d67afc05",
|
||||
"moduleName": "Iou",
|
||||
"entityName": "IouTransfer"
|
||||
},
|
||||
"witnessParties": [
|
||||
"Alice"
|
||||
],
|
||||
"argument": {
|
||||
"iou": {
|
||||
"observers": [],
|
||||
"issuer": "Alice",
|
||||
"amount": "999.99",
|
||||
"currency": "USD",
|
||||
"owner": "Alice"
|
||||
"archived": {
|
||||
"workflowId": "Alice Workflow",
|
||||
"contractId": "#56:0",
|
||||
"templateId": {
|
||||
"packageId": "ac3a64908d9f6b4453329b3d7d8ddea44c83f4f5469de5f7ae19158c69bf8473",
|
||||
"moduleName": "Iou",
|
||||
"entityName": "Iou"
|
||||
},
|
||||
"newOwner": "Alice"
|
||||
},
|
||||
"workflowId": "Alice Workflow"
|
||||
"witnessParties": [
|
||||
"Alice"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"active": {
|
||||
"observers": [],
|
||||
"agreementText": "",
|
||||
"signatories": [
|
||||
"Alice"
|
||||
],
|
||||
"contractId": "#301:1",
|
||||
"templateId": {
|
||||
"packageId": "ac3a64908d9f6b4453329b3d7d8ddea44c83f4f5469de5f7ae19158c69bf8473",
|
||||
"moduleName": "Iou",
|
||||
"entityName": "IouTransfer"
|
||||
},
|
||||
"witnessParties": [
|
||||
"Alice"
|
||||
],
|
||||
"argument": {
|
||||
"iou": {
|
||||
"observers": [],
|
||||
"issuer": "Alice",
|
||||
"amount": "999.99",
|
||||
"currency": "USD",
|
||||
"owner": "Alice"
|
||||
},
|
||||
"newOwner": "Alice"
|
||||
},
|
||||
"workflowId": "Alice Workflow"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,3 +1,9 @@
|
||||
# HTTP JSON Service
|
||||
|
||||
See "HTTP JSON API Service" on docs.daml.com for usage information.
|
||||
|
||||
Documentation can also be found in the RST format:
|
||||
- [HTTP JSON API Service](/docs/source/json-api/index.rst)
|
||||
- [DAML-LF JSON Encoding](/docs/source/json-api/lf-value-specification.rst)
|
||||
- [Search Query Language](/docs/source/json-api/search-query-language.rst)
|
||||
|
||||
|
@ -8,8 +8,14 @@ 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.domain.{ActiveContract, CreateCommand, ExerciseCommand, JwtPayload}
|
||||
import com.digitalasset.http.util.ClientUtil.{uniqueCommandId, workflowIdFromParty}
|
||||
import com.digitalasset.http.domain.{
|
||||
ActiveContract,
|
||||
Contract,
|
||||
CreateCommand,
|
||||
ExerciseCommand,
|
||||
JwtPayload
|
||||
}
|
||||
import com.digitalasset.http.util.ClientUtil.uniqueCommandId
|
||||
import com.digitalasset.http.util.IdentifierConverters.refApiIdentifier
|
||||
import com.digitalasset.http.util.{Commands, Transactions}
|
||||
import com.digitalasset.jwt.domain.Jwt
|
||||
@ -19,6 +25,7 @@ import com.typesafe.scalalogging.StrictLogging
|
||||
import scalaz.std.scalaFuture._
|
||||
import scalaz.syntax.show._
|
||||
import scalaz.syntax.std.option._
|
||||
import scalaz.syntax.traverse._
|
||||
import scalaz.{-\/, EitherT, Show, \/, \/-}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
@ -50,13 +57,13 @@ class CommandService(
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Any"))
|
||||
def exercise(jwt: Jwt, jwtPayload: JwtPayload, input: ExerciseCommand[lav1.value.Record])
|
||||
: Future[Error \/ ImmArraySeq[ActiveContract[lav1.value.Value]]] = {
|
||||
: Future[Error \/ ImmArraySeq[Contract[lav1.value.Value]]] = {
|
||||
|
||||
val et: EitherT[Future, Error, ImmArraySeq[ActiveContract[lav1.value.Value]]] = for {
|
||||
val et: EitherT[Future, Error, ImmArraySeq[Contract[lav1.value.Value]]] = for {
|
||||
command <- EitherT.either(exerciseCommand(input))
|
||||
request = submitAndWaitRequest(jwtPayload, input.meta, command)
|
||||
response <- liftET(logResult('exercise, submitAndWaitForTransaction(jwt, request)))
|
||||
contracts <- EitherT.either(activeContracts(response))
|
||||
contracts <- EitherT.either(contracts(response))
|
||||
} yield contracts
|
||||
|
||||
et.run
|
||||
@ -98,8 +105,7 @@ class CommandService(
|
||||
val maximumRecordTime: Instant = meta
|
||||
.flatMap(_.maximumRecordTime)
|
||||
.getOrElse(ledgerEffectiveTime.plusNanos(defaultTimeToLive.toNanos))
|
||||
val workflowId: domain.WorkflowId =
|
||||
meta.flatMap(_.workflowId).getOrElse(workflowIdFromParty(jwtPayload.party))
|
||||
val workflowId: Option[domain.WorkflowId] = meta.flatMap(_.workflowId)
|
||||
val commandId: lar.CommandId = meta.flatMap(_.commandId).getOrElse(uniqueCommandId())
|
||||
|
||||
Commands.submitAndWaitRequest(
|
||||
@ -133,16 +139,28 @@ class CommandService(
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Any"))
|
||||
private def activeContracts(
|
||||
tx: lav1.transaction.Transaction): Error \/ ImmArraySeq[ActiveContract[lav1.value.Value]] = {
|
||||
|
||||
import scalaz.syntax.traverse._
|
||||
|
||||
val workflowId = domain.WorkflowId.fromLedgerApi(tx)
|
||||
|
||||
Transactions
|
||||
.decodeAllCreatedEvents(tx)
|
||||
.traverse(domain.ActiveContract.fromLedgerApi(workflowId)(_))
|
||||
.traverse(ActiveContract.fromLedgerApi(workflowId)(_))
|
||||
.leftMap(e => Error('activeContracts, e.shows))
|
||||
}
|
||||
|
||||
private def contracts(response: lav1.command_service.SubmitAndWaitForTransactionResponse)
|
||||
: Error \/ ImmArraySeq[Contract[lav1.value.Value]] =
|
||||
response.transaction
|
||||
.toRightDisjunction(Error('contracts, s"Received response without transaction: $response"))
|
||||
.flatMap(contracts)
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Any"))
|
||||
private def contracts(
|
||||
tx: lav1.transaction.Transaction): Error \/ ImmArraySeq[Contract[lav1.value.Value]] = {
|
||||
val workflowId = domain.WorkflowId.fromLedgerApi(tx)
|
||||
tx.events.iterator
|
||||
.to[ImmArraySeq]
|
||||
.traverse(Contract.fromLedgerApi(workflowId)(_))
|
||||
.leftMap(e => Error('contracts, e.shows))
|
||||
}
|
||||
}
|
||||
|
||||
object CommandService {
|
||||
|
@ -66,7 +66,7 @@ class ContractsService(
|
||||
jwt: Jwt,
|
||||
party: lar.Party,
|
||||
templateId: Option[TemplateId.OptionalPkg],
|
||||
contractId: String): Future[Option[ActiveContract]] =
|
||||
contractId: domain.ContractId): Future[Option[ActiveContract]] =
|
||||
for {
|
||||
(as, _) <- search(jwt, party, templateIds(templateId), Map.empty)
|
||||
a = findByContractId(contractId)(as)
|
||||
@ -75,8 +75,9 @@ class ContractsService(
|
||||
private def templateIds(a: Option[TemplateId.OptionalPkg]): Set[TemplateId.OptionalPkg] =
|
||||
a.toList.toSet
|
||||
|
||||
private def findByContractId(k: String)(as: Seq[ActiveContract]): Option[ActiveContract] =
|
||||
as.find(x => (x.contractId: String) == k)
|
||||
private def findByContractId(k: domain.ContractId)(
|
||||
as: Seq[ActiveContract]): Option[ActiveContract] =
|
||||
as.find(x => (x.contractId: domain.ContractId) == k)
|
||||
|
||||
def search(jwt: Jwt, jwtPayload: JwtPayload, request: GetActiveContractsRequest): Future[Result] =
|
||||
search(jwt, jwtPayload.party, request.templateIds, request.query)
|
||||
|
@ -85,12 +85,12 @@ class Endpoints(
|
||||
.leftMap(e => InvalidUserInput(e.shows))
|
||||
): ET[domain.ExerciseCommand[lav1.value.Record]]
|
||||
|
||||
as <- eitherT(
|
||||
cs <- eitherT(
|
||||
handleFutureFailure(commandService.exercise(jwt, jwtPayload, cmd))
|
||||
): ET[ImmArraySeq[domain.ActiveContract[lav1.value.Value]]]
|
||||
): ET[ImmArraySeq[domain.Contract[lav1.value.Value]]]
|
||||
|
||||
jsVal <- either(
|
||||
as.traverse(a => encoder.encodeV(a))
|
||||
cs.traverse(a => encoder.encodeV(a))
|
||||
.leftMap(e => ServerError(e.shows))
|
||||
.flatMap(as => encodeList(as))): ET[JsValue]
|
||||
|
||||
|
@ -30,22 +30,32 @@ object domain {
|
||||
}
|
||||
}
|
||||
|
||||
case class JwtPayload(ledgerId: lar.LedgerId, applicationId: lar.ApplicationId, party: lar.Party)
|
||||
case class JwtPayload(ledgerId: lar.LedgerId, applicationId: lar.ApplicationId, party: Party)
|
||||
|
||||
case class TemplateId[+PkgId](packageId: PkgId, moduleName: String, entityName: String)
|
||||
|
||||
case class Contract[+LfV](value: ArchivedContract \/ ActiveContract[LfV])
|
||||
|
||||
case class ActiveContract[+LfV](
|
||||
workflowId: Option[WorkflowId],
|
||||
contractId: String,
|
||||
contractId: ContractId,
|
||||
templateId: TemplateId.RequiredPkg,
|
||||
key: Option[LfV],
|
||||
argument: LfV,
|
||||
witnessParties: Seq[String],
|
||||
witnessParties: Seq[Party],
|
||||
signatories: Seq[Party],
|
||||
observers: Seq[Party],
|
||||
agreementText: String)
|
||||
|
||||
case class ArchivedContract(
|
||||
workflowId: Option[WorkflowId],
|
||||
contractId: ContractId,
|
||||
templateId: TemplateId.RequiredPkg,
|
||||
witnessParties: Seq[Party])
|
||||
|
||||
case class ContractLookupRequest[+LfV](
|
||||
ledgerId: Option[String],
|
||||
id: (TemplateId.OptionalPkg, LfV) \/ (Option[TemplateId.OptionalPkg], String))
|
||||
id: (TemplateId.OptionalPkg, LfV) \/ (Option[TemplateId.OptionalPkg], ContractId))
|
||||
|
||||
case class GetActiveContractsRequest(
|
||||
templateIds: Set[TemplateId.OptionalPkg],
|
||||
@ -54,6 +64,14 @@ object domain {
|
||||
type WorkflowIdTag = lar.WorkflowIdTag
|
||||
type WorkflowId = lar.WorkflowId
|
||||
|
||||
type ContractIdTag = lar.ContractIdTag
|
||||
type ContractId = lar.ContractId
|
||||
val ContractId = lar.ContractId
|
||||
|
||||
type PartyTag = lar.PartyTag
|
||||
type Party = lar.Party
|
||||
val Party = lar.Party
|
||||
|
||||
object WorkflowId {
|
||||
|
||||
def apply(s: String): WorkflowId = lar.WorkflowId(s)
|
||||
@ -77,21 +95,52 @@ object domain {
|
||||
TemplateId(in.packageId, in.moduleName, in.entityName)
|
||||
}
|
||||
|
||||
object Contract {
|
||||
def fromLedgerApi(workflowId: Option[WorkflowId])(
|
||||
event: lav1.event.Event): Error \/ Contract[lav1.value.Value] = event.event match {
|
||||
case lav1.event.Event.Event.Created(created) =>
|
||||
ActiveContract.fromLedgerApi(workflowId)(created).map(a => Contract(\/-(a)))
|
||||
case lav1.event.Event.Event.Archived(archived) =>
|
||||
ArchivedContract.fromLedgerApi(workflowId)(archived).map(a => Contract(-\/(a)))
|
||||
case lav1.event.Event.Event.Empty =>
|
||||
val errorMsg = s"Expected either Created or Archived event, got: Empty"
|
||||
-\/(Error('Contract_fromLedgerApi, errorMsg))
|
||||
}
|
||||
|
||||
implicit val covariant: Traverse[Contract] = new Traverse[Contract] {
|
||||
|
||||
override def map[A, B](fa: Contract[A])(f: A => B): Contract[B] = {
|
||||
val valueB: ArchivedContract \/ ActiveContract[B] = fa.value.map(a => a.map(f))
|
||||
Contract(valueB)
|
||||
}
|
||||
|
||||
override def traverseImpl[G[_]: Applicative, A, B](fa: Contract[A])(
|
||||
f: A => G[B]): G[Contract[B]] = {
|
||||
val valueB: G[ArchivedContract \/ ActiveContract[B]] = fa.value.traverse(a => a.traverse(f))
|
||||
valueB.map(x => Contract[B](x))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def boxedRecord(a: lav1.value.Record): lav1.value.Value =
|
||||
lav1.value.Value(lav1.value.Value.Sum.Record(a))
|
||||
|
||||
object ActiveContract {
|
||||
def fromLedgerApi(workflowId: Option[WorkflowId])(
|
||||
in: lav1.event.CreatedEvent): Error \/ ActiveContract[lav1.value.Value] =
|
||||
for {
|
||||
templateId <- in.templateId required "templateId"
|
||||
argument <- in.createArguments required "createArguments"
|
||||
boxedArgument = lav1.value.Value(lav1.value.Value.Sum.Record(argument))
|
||||
} yield
|
||||
ActiveContract(
|
||||
workflowId = workflowId,
|
||||
contractId = in.contractId,
|
||||
contractId = ContractId(in.contractId),
|
||||
templateId = TemplateId fromLedgerApi templateId,
|
||||
key = in.contractKey,
|
||||
argument = boxedArgument,
|
||||
witnessParties = in.witnessParties,
|
||||
argument = boxedRecord(argument),
|
||||
witnessParties = Party.subst(in.witnessParties),
|
||||
signatories = Party.subst(in.signatories),
|
||||
observers = Party.subst(in.observers),
|
||||
agreementText = in.agreementText getOrElse ""
|
||||
)
|
||||
|
||||
@ -123,6 +172,20 @@ object domain {
|
||||
}
|
||||
}
|
||||
|
||||
object ArchivedContract {
|
||||
def fromLedgerApi(workflowId: Option[WorkflowId])(
|
||||
in: lav1.event.ArchivedEvent): Error \/ ArchivedContract =
|
||||
for {
|
||||
templateId <- in.templateId required "templateId"
|
||||
} yield
|
||||
ArchivedContract(
|
||||
workflowId = workflowId,
|
||||
contractId = ContractId(in.contractId),
|
||||
templateId = TemplateId fromLedgerApi templateId,
|
||||
witnessParties = Party.subst(in.witnessParties)
|
||||
)
|
||||
}
|
||||
|
||||
object ContractLookupRequest {
|
||||
implicit val covariant: Traverse[ContractLookupRequest] = new Traverse[ContractLookupRequest] {
|
||||
override def map[A, B](fa: ContractLookupRequest[A])(f: A => B) =
|
||||
|
@ -24,16 +24,16 @@ object JsonProtocol extends DefaultJsonProtocol {
|
||||
implicit val WorkflowIdFormat: JsonFormat[domain.WorkflowId] =
|
||||
taggedJsonFormat[String, domain.WorkflowIdTag]
|
||||
|
||||
implicit val PartyFormat: JsonFormat[lar.Party] =
|
||||
taggedJsonFormat[String, lar.PartyTag]
|
||||
implicit val PartyFormat: JsonFormat[domain.Party] =
|
||||
taggedJsonFormat[String, domain.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 ContractIdFormat: JsonFormat[domain.ContractId] =
|
||||
taggedJsonFormat[String, domain.ContractIdTag]
|
||||
|
||||
object LfValueCodec
|
||||
extends ApiCodecCompressed[AbsoluteContractId](
|
||||
@ -69,7 +69,9 @@ object JsonProtocol extends DefaultJsonProtocol {
|
||||
case (Some(templateId), Some(key), None) =>
|
||||
-\/((templateId.convertTo[domain.TemplateId.OptionalPkg], key))
|
||||
case (otid, None, Some(contractId)) =>
|
||||
\/-((otid map (_.convertTo[domain.TemplateId.OptionalPkg]), contractId.convertTo[String]))
|
||||
val a = otid map (_.convertTo[domain.TemplateId.OptionalPkg])
|
||||
val b = contractId.convertTo[domain.ContractId]
|
||||
\/-((a, b))
|
||||
case (None, Some(_), None) =>
|
||||
deserializationError(
|
||||
"ContractLookupRequest requires key to be accompanied by a templateId")
|
||||
@ -80,8 +82,36 @@ object JsonProtocol extends DefaultJsonProtocol {
|
||||
case _ => deserializationError("ContractLookupRequest must be an object")
|
||||
}
|
||||
|
||||
implicit val ContractFormat: RootJsonFormat[domain.Contract[JsValue]] =
|
||||
new RootJsonFormat[domain.Contract[JsValue]] {
|
||||
private val archivedKey = "archived"
|
||||
private val activeKey = "active"
|
||||
|
||||
override def read(json: JsValue): domain.Contract[JsValue] = json match {
|
||||
case JsObject(fields) =>
|
||||
fields.toList match {
|
||||
case List((`archivedKey`, archived)) =>
|
||||
domain.Contract(-\/(ArchivedContractFormat.read(archived)))
|
||||
case List((`activeKey`, active)) =>
|
||||
domain.Contract(\/-(ActiveContractFormat.read(active)))
|
||||
case _ =>
|
||||
deserializationError(
|
||||
s"Contract must be either {$archivedKey: obj} or {$activeKey: obj}, got: $fields")
|
||||
}
|
||||
case _ => deserializationError("Contract must be an object")
|
||||
}
|
||||
|
||||
override def write(obj: domain.Contract[JsValue]): JsValue = obj.value match {
|
||||
case -\/(archived) => JsObject(archivedKey -> ArchivedContractFormat.write(archived))
|
||||
case \/-(active) => JsObject(activeKey -> ActiveContractFormat.write(active))
|
||||
}
|
||||
}
|
||||
|
||||
implicit val ActiveContractFormat: RootJsonFormat[domain.ActiveContract[JsValue]] =
|
||||
jsonFormat7(domain.ActiveContract.apply[JsValue])
|
||||
jsonFormat9(domain.ActiveContract.apply[JsValue])
|
||||
|
||||
implicit val ArchivedContractFormat: RootJsonFormat[domain.ArchivedContract] =
|
||||
jsonFormat4(domain.ArchivedContract.apply)
|
||||
|
||||
private val templatesKey = "%templates"
|
||||
|
||||
|
@ -8,14 +8,8 @@ import akka.stream.Materializer
|
||||
import akka.stream.scaladsl.{Sink, Source}
|
||||
import akka.{Done, NotUsed}
|
||||
import com.digitalasset.api.util.TimeProvider
|
||||
import com.digitalasset.http.domain
|
||||
import com.digitalasset.http.util.FutureUtil.toFuture
|
||||
import com.digitalasset.ledger.api.refinements.ApiTypes.{
|
||||
ApplicationId,
|
||||
CommandId,
|
||||
Party,
|
||||
WorkflowId
|
||||
}
|
||||
import com.digitalasset.ledger.api.refinements.ApiTypes.{ApplicationId, CommandId, Party}
|
||||
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}
|
||||
@ -65,8 +59,5 @@ object ClientUtil {
|
||||
|
||||
def uniqueId(): String = UUID.randomUUID.toString
|
||||
|
||||
def workflowIdFromParty(p: Party): domain.WorkflowId =
|
||||
WorkflowId(s"${Party.unwrap(p)} Workflow")
|
||||
|
||||
def uniqueCommandId(): CommandId = CommandId(uniqueId())
|
||||
}
|
||||
|
@ -53,16 +53,18 @@ object Commands {
|
||||
def submitAndWaitRequest(
|
||||
ledgerId: lar.LedgerId,
|
||||
applicationId: lar.ApplicationId,
|
||||
workflowId: domain.WorkflowId,
|
||||
workflowId: Option[domain.WorkflowId],
|
||||
commandId: lar.CommandId,
|
||||
ledgerEffectiveTime: Instant,
|
||||
maximumRecordTime: Instant,
|
||||
party: lar.Party,
|
||||
command: lav1.commands.Command.Command): lav1.command_service.SubmitAndWaitRequest = {
|
||||
|
||||
val workflowIdStr: String = workflowId.map(domain.WorkflowId.unwrap).getOrElse("")
|
||||
|
||||
val commands = lav1.commands.Commands(
|
||||
ledgerId = lar.LedgerId.unwrap(ledgerId),
|
||||
workflowId = domain.WorkflowId.unwrap(workflowId),
|
||||
workflowId = workflowIdStr,
|
||||
applicationId = lar.ApplicationId.unwrap(applicationId),
|
||||
commandId = lar.CommandId.unwrap(commandId),
|
||||
party = lar.Party.unwrap(party),
|
||||
|
@ -5,6 +5,8 @@ package com.digitalasset.http
|
||||
|
||||
import com.digitalasset.ledger.api.{v1 => lav1}
|
||||
import org.scalacheck.Gen
|
||||
import scalaz.{-\/, \/, \/-}
|
||||
import spray.json.{JsString, JsValue}
|
||||
|
||||
object Generators {
|
||||
def genApiIdentifier: Gen[lav1.value.Identifier] =
|
||||
@ -51,4 +53,52 @@ object Generators {
|
||||
implicit object OptionalPackageIdGen extends PackageIdGen[Option[String]] {
|
||||
override def gen: Gen[Option[String]] = Gen.option(RequiredPackageIdGen.gen)
|
||||
}
|
||||
|
||||
def workflowIdGen: Gen[domain.WorkflowId] = Gen.identifier.map(domain.WorkflowId(_))
|
||||
def contractIdGen: Gen[domain.ContractId] = Gen.identifier.map(domain.ContractId(_))
|
||||
def partyGen: Gen[domain.Party] = Gen.identifier.map(domain.Party(_))
|
||||
|
||||
def scalazEitherGen[A, B](a: Gen[A], b: Gen[B]): Gen[\/[A, B]] =
|
||||
Gen.oneOf(a.map(-\/(_)), b.map(\/-(_)))
|
||||
|
||||
def contractGen: Gen[domain.Contract[JsValue]] =
|
||||
scalazEitherGen(archivedContractGen, activeContractGen).map(domain.Contract(_))
|
||||
|
||||
def activeContractGen: Gen[domain.ActiveContract[JsValue]] =
|
||||
for {
|
||||
workflowId <- Gen.option(workflowIdGen)
|
||||
contractId <- contractIdGen
|
||||
templateId <- Generators.genDomainTemplateId
|
||||
key <- Gen.option(Gen.identifier.map(JsString(_)))
|
||||
argument <- Gen.identifier.map(JsString(_))
|
||||
witnessParties <- Gen.listOf(partyGen)
|
||||
signatories <- Gen.listOf(partyGen)
|
||||
observers <- Gen.listOf(partyGen)
|
||||
agreementText <- Gen.identifier
|
||||
} yield
|
||||
domain.ActiveContract[JsValue](
|
||||
workflowId = workflowId,
|
||||
contractId = contractId,
|
||||
templateId = templateId,
|
||||
key = key,
|
||||
argument = argument,
|
||||
witnessParties = witnessParties,
|
||||
signatories = signatories,
|
||||
observers = observers,
|
||||
agreementText = agreementText
|
||||
)
|
||||
|
||||
def archivedContractGen: Gen[domain.ArchivedContract] =
|
||||
for {
|
||||
workflowId <- Gen.option(workflowIdGen)
|
||||
contractId <- contractIdGen
|
||||
templateId <- Generators.genDomainTemplateId
|
||||
witnessParties <- Gen.listOf(partyGen)
|
||||
} yield
|
||||
domain.ArchivedContract(
|
||||
workflowId = workflowId,
|
||||
contractId = contractId,
|
||||
templateId = templateId,
|
||||
witnessParties = witnessParties,
|
||||
)
|
||||
}
|
||||
|
@ -271,8 +271,15 @@ class HttpServiceIntegrationTest
|
||||
inside(exerciseOutput) {
|
||||
case JsObject(fields) =>
|
||||
inside(fields.get("result")) {
|
||||
case Some(JsArray(Vector(activeContract: JsObject))) =>
|
||||
assertActiveContract(decoder, activeContract, create, exercise)
|
||||
case Some(JsArray(Vector(contract1: JsObject, contract2: JsObject))) =>
|
||||
inside(contract1.fields.toList) {
|
||||
case List(("archived", archived: JsObject)) =>
|
||||
assertArchivedContract(archived, contractId)
|
||||
}
|
||||
inside(contract2.fields.toList) {
|
||||
case List(("active", active: JsObject)) =>
|
||||
assertActiveContract(decoder, active, create, exercise)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -296,20 +303,35 @@ class HttpServiceIntegrationTest
|
||||
}: Future[Assertion]
|
||||
}
|
||||
|
||||
private def assertArchivedContract(
|
||||
jsObject: JsObject,
|
||||
contractId: domain.ContractId): Assertion = {
|
||||
import JsonProtocol._
|
||||
val archived = SprayJson.decode[domain.ArchivedContract](jsObject).valueOr(e => fail(e.shows))
|
||||
archived.contractId shouldBe contractId
|
||||
}
|
||||
|
||||
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")
|
||||
val expectedContractFields: Seq[v.RecordField] = create.argument.fields
|
||||
val expectedNewOwner: v.Value = exercise.argument.fields.headOption
|
||||
.flatMap(_.value)
|
||||
.getOrElse(fail("Cannot extract expected newOwner"))
|
||||
|
||||
inside(jsObject.fields.get("argument")) {
|
||||
case Some(JsObject(fields)) =>
|
||||
fields.size shouldBe (exercise.argument.fields.size + 1) // +1 for the original "iou" from create
|
||||
val active = decoder.decodeV[domain.ActiveContract](jsObject).valueOr(e => fail(e.shows))
|
||||
inside(active.argument.sum.record.map(_.fields)) {
|
||||
case Some(
|
||||
Seq(
|
||||
v.RecordField("iou", Some(contractRecord)),
|
||||
v.RecordField("newOwner", Some(newOwner)))) =>
|
||||
val contractFields: Seq[v.RecordField] =
|
||||
contractRecord.sum.record.map(_.fields).getOrElse(Seq.empty)
|
||||
(contractFields: Seq[v.RecordField]) shouldBe (expectedContractFields: Seq[v.RecordField])
|
||||
(newOwner: v.Value) shouldBe (expectedNewOwner: v.Value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -454,13 +476,13 @@ class HttpServiceIntegrationTest
|
||||
}
|
||||
}
|
||||
|
||||
private def getContractId(output: JsValue): lar.ContractId =
|
||||
private def getContractId(output: JsValue): domain.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)
|
||||
case Some(JsString(contractId)) => domain.ContractId(contractId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2019 The DAML Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.http.json
|
||||
|
||||
import com.digitalasset.http.Generators.contractGen
|
||||
import com.digitalasset.http.domain
|
||||
import org.scalatest.prop.GeneratorDrivenPropertyChecks
|
||||
import org.scalatest.{FreeSpec, Inside, Matchers}
|
||||
import scalaz.{\/, \/-}
|
||||
import spray.json.{JsObject, JsValue}
|
||||
|
||||
class JsonProtocolTest
|
||||
extends FreeSpec
|
||||
with Matchers
|
||||
with Inside
|
||||
with GeneratorDrivenPropertyChecks {
|
||||
|
||||
import JsonProtocol._
|
||||
|
||||
implicit override val generatorDrivenConfig: PropertyCheckConfiguration =
|
||||
PropertyCheckConfiguration(minSuccessful = 100)
|
||||
|
||||
"domain.Contract" - {
|
||||
|
||||
"can be serialized to JSON" in forAll(contractGen) { contract =>
|
||||
inside(SprayJson.encode(contract)) {
|
||||
case \/-(JsObject(fields)) =>
|
||||
inside(fields.toList) {
|
||||
case List(("archived", JsObject(_))) =>
|
||||
case List(("active", JsObject(_))) =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"can be serialized and deserialized back to the same object" in forAll(contractGen) {
|
||||
contract0 =>
|
||||
val actual: SprayJson.Error \/ domain.Contract[JsValue] = for {
|
||||
jsValue <- SprayJson.encode(contract0)
|
||||
contract <- SprayJson.decode[domain.Contract[JsValue]](jsValue)
|
||||
} yield contract
|
||||
|
||||
inside(actual) {
|
||||
case \/-(contract1) => contract1 shouldBe contract0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,8 @@ This page contains release notes for the SDK.
|
||||
HEAD — ongoing
|
||||
--------------
|
||||
|
||||
+ [JSON API - Experimental] Returning archived and active contracts from ``/command/exercise``
|
||||
enpoint. See `issue #2925 <https://github.com/digital-asset/daml/issues/2925>`_.
|
||||
+ [JSON API - Experimental] Flattening the output of the ``/contracts/search`` endpoint.
|
||||
The endpoint returns ``ActiveContract`` objects without ``GetActiveContractsResponse`` wrappers.
|
||||
See `issue #2987 <https://github.com/digital-asset/daml/pull/2987>`_.
|
||||
|
Loading…
Reference in New Issue
Block a user