diff --git a/ledger-service/db-backend/src/main/scala/com/digitalasset/http/dbbackend/Queries.scala b/ledger-service/db-backend/src/main/scala/com/digitalasset/http/dbbackend/Queries.scala index 61b99128cf72..932f38ca3e02 100644 --- a/ledger-service/db-backend/src/main/scala/com/digitalasset/http/dbbackend/Queries.scala +++ b/ledger-service/db-backend/src/main/scala/com/digitalasset/http/dbbackend/Queries.scala @@ -684,7 +684,7 @@ private final class PostgresQueries(tablePrefix: String, tpIdCacheMaxEntries: Lo private[this] val contractKeyHashIndexName = Fragment.const0(s"${tablePrefix}ckey_hash_idx") private[this] val contractKeyHashIndex = CreateIndex( - sql"""CREATE UNIQUE INDEX $contractKeyHashIndexName ON $contractTableName (key_hash)""" + sql"""CREATE INDEX $contractKeyHashIndexName ON $contractTableName (key_hash)""" ) protected[this] override def extraDatabaseDdls = @@ -845,7 +845,7 @@ private final class OracleQueries( private[this] val contractKeyHashIndexName = Fragment.const0(s"${tablePrefix}ckey_hash_idx") private[this] val contractKeyHashIndex = CreateIndex( - sql"""CREATE UNIQUE INDEX $contractKeyHashIndexName ON $contractTableName (key_hash)""" + sql"""CREATE INDEX $contractKeyHashIndexName ON $contractTableName (key_hash)""" ) private[this] val indexPayload = CreateIndex(sql""" diff --git a/ledger-service/http-json-oracle/BUILD.bazel b/ledger-service/http-json-oracle/BUILD.bazel index 39729b1a7618..873a788840b3 100644 --- a/ledger-service/http-json-oracle/BUILD.bazel +++ b/ledger-service/http-json-oracle/BUILD.bazel @@ -22,6 +22,7 @@ da_scala_test( data = [ "//docs:quickstart-model.dar", "//ledger-service/http-json:Account.dar", + "//ledger-service/http-json:User.dar", "//ledger/test-common:dar-files", "//ledger/test-common/test-certificates", ], diff --git a/ledger-service/http-json/BUILD.bazel b/ledger-service/http-json/BUILD.bazel index f00657f4d61d..69a7be8b8f56 100644 --- a/ledger-service/http-json/BUILD.bazel +++ b/ledger-service/http-json/BUILD.bazel @@ -185,6 +185,12 @@ daml_compile( visibility = ["//ledger-service:__subpackages__"], ) +daml_compile( + name = "User", + srcs = ["src/it/daml/User.daml"], + visibility = ["//ledger-service:__subpackages__"], +) + [ da_scala_test( name = "tests-{}".format(edition), @@ -337,6 +343,7 @@ alias( ] if scala_major_version == "2.12" else [], data = [ ":Account.dar", + ":User.dar", "//docs:quickstart-model.dar", "//ledger/test-common:dar-files", "//ledger/test-common/test-certificates", diff --git a/ledger-service/http-json/src/it/daml/User.daml b/ledger-service/http-json/src/it/daml/User.daml new file mode 100644 index 000000000000..d964ba6a2b77 --- /dev/null +++ b/ledger-service/http-json/src/it/daml/User.daml @@ -0,0 +1,27 @@ +-- Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +module User where + +-- MAIN_TEMPLATE_BEGIN +template User with + username: Party + following: [Party] + where + signatory username + observer following +-- MAIN_TEMPLATE_END + + key username: Party + maintainer key + + -- FOLLOW_BEGIN + nonconsuming choice Follow: ContractId User with + userToFollow: Party + controller username + do + assertMsg "You cannot follow yourself" (userToFollow /= username) + assertMsg "You cannot follow the same user twice" (notElem userToFollow following) + archive self + create this with following = userToFollow :: following + -- FOLLOW_END diff --git a/ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala b/ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala index c72a40e14ea4..e91937cb303a 100644 --- a/ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala +++ b/ledger-service/http-json/src/itlib/scala/http/AbstractHttpServiceIntegrationTest.scala @@ -63,6 +63,8 @@ object AbstractHttpServiceIntegrationTestFuns { private[http] val dar3 = requiredResource(SemanticTestDar.path) + private[http] val userDar = requiredResource("ledger-service/http-json/User.dar") + def sha256(source: Source[ByteString, Any])(implicit mat: Materializer): Try[String] = Try { import java.security.MessageDigest import javax.xml.bind.DatatypeConverter @@ -103,6 +105,9 @@ trait AbstractHttpServiceIntegrationTestFuns protected val metadata2: MetadataReader.LfMetadata = MetadataReader.readFromDar(dar2).valueOr(e => fail(s"Cannot read dar2 metadata: $e")) + protected val metadataUser: MetadataReader.LfMetadata = + MetadataReader.readFromDar(userDar).valueOr(e => fail(s"Cannot read userDar metadata: $e")) + protected val jwt: Jwt = jwtForParties(List("Alice"), List(), testId) protected val jwtAdminNoParty: Jwt = { @@ -128,7 +133,7 @@ trait AbstractHttpServiceIntegrationTestFuns import tag.@@ // used for subtyping to make `AHS ec` beat executionContext implicit val `AHS ec`: ExecutionContext @@ this.type = tag[this.type](`AHS asys`.dispatcher) - override def packageFiles = List(dar1, dar2) + override def packageFiles = List(dar1, dar2, userDar) protected def getUniqueParty(name: String) = getUniquePartyAndAuthHeaders(name)._1 protected def getUniquePartyAndAuthHeaders(name: String): (domain.Party, List[HttpHeader]) = { @@ -274,7 +279,7 @@ trait AbstractHttpServiceIntegrationTestFuns at[K :->>: V]((fn.value.name, _)) } - private[this] def recordFromFields[L <: HList, I <: HList](hlist: L)(implicit + protected[this] def recordFromFields[L <: HList, I <: HList](hlist: L)(implicit mapper: shapeless.ops.hlist.Mapper.Aux[RecordFromFields.type, L, I], lister: shapeless.ops.hlist.ToTraversable.Aux[I, Seq, (String, v.Value.Sum)], ): v.Record = v.Record(fields = hlist.map(RecordFromFields).to[Seq].map { case (n, vs) => @@ -1822,4 +1827,106 @@ abstract class AbstractHttpServiceIntegrationTest _ <- queryN(0) } yield succeed } + + "Should ignore conflicts on contract key hash constraint violation" in withHttpServiceAndClient { + (uri, encoder, _, _, _) => + import scalaz.std.vector._ + import scalaz.syntax.tag._ + import scalaz.syntax.traverse._ + import scalaz.std.scalaFuture._ + import shapeless.record.{Record => ShRecord} + import com.daml.ledger.api.refinements.{ApiTypes => lar} + + val partyIds = Vector("Alice", "Bob").map(getUniqueParty) + val packageId: Ref.PackageId = MetadataReader + .templateByName(metadataUser)(Ref.QualifiedName.assertFromString("User:User")) + .collectFirst { case (pkgid, _) => pkgid } + .getOrElse(fail(s"Cannot retrieve packageId")) + + def userCreateCommand( + username: domain.Party, + following: Seq[domain.Party] = Seq.empty, + ): domain.CreateCommand[v.Record, domain.TemplateId.OptionalPkg] = { + val templateId = domain.TemplateId(None, "User", "User") + val followingList = following.map(party => v.Value(v.Value.Sum.Party(party.unwrap))) + val arg = recordFromFields( + ShRecord( + username = v.Value.Sum.Party(username.unwrap), + following = v.Value.Sum.List(v.List.of(followingList)), + ) + ) + + domain.CreateCommand(templateId, arg, None) + } + def userExerciseFollowCommand( + contractId: lar.ContractId, + toFollow: domain.Party, + ): domain.ExerciseCommand[v.Value, domain.EnrichedContractId] = { + val templateId = domain.TemplateId(None, "User", "User") + val reference = domain.EnrichedContractId(Some(templateId), contractId) + val arg = recordFromFields(ShRecord(userToFollow = v.Value.Sum.Party(toFollow.unwrap))) + val choice = lar.Choice("Follow") + + domain.ExerciseCommand(reference, choice, boxedRecord(arg), None) + } + + def followUser(contractId: lar.ContractId, actAs: domain.Party, toFollow: domain.Party) = { + val exercise: domain.ExerciseCommand[v.Value, domain.EnrichedContractId] = + userExerciseFollowCommand(contractId, toFollow) + val exerciseJson: JsValue = encodeExercise(encoder)(exercise) + + postJsonRequest( + uri.withPath(Uri.Path("/v1/exercise")), + exerciseJson, + headers = headersWithPartyAuth(actAs = List(actAs.unwrap)), + ) + .map { case (exerciseStatus, exerciseOutput) => + exerciseStatus shouldBe StatusCodes.OK + assertStatus(exerciseOutput, StatusCodes.OK) + () + } + + } + + def queryUsers(fromPerspectiveOfParty: domain.Party) = { + val query = jsObject(s"""{ + "templateIds": ["$packageId:User:User"], + "query": {} + }""") + + postJsonRequest( + uri.withPath(Uri.Path("/v1/query")), + query, + headers = headersWithPartyAuth(actAs = List(fromPerspectiveOfParty.unwrap)), + ).map { case (searchStatus, searchOutput) => + searchStatus shouldBe StatusCodes.OK + assertStatus(searchOutput, StatusCodes.OK) + } + } + val commands = partyIds.map { p => + (p, userCreateCommand(p)) + } + + for { + users <- commands.traverse { case (party, command) => + val fut = postCreateCommand( + command, + encoder, + uri, + headers = headersWithPartyAuth(actAs = List(party.unwrap)), + ).map { case (status, output) => + status shouldBe StatusCodes.OK + assertStatus(output, StatusCodes.OK) + getContractId(getResult(output)) + }: Future[ContractId] + fut.map(cid => (party, cid)) + } + (alice, aliceUserId) = users(0) + (bob, bobUserId) = users(1) + _ <- followUser(aliceUserId, alice, bob) + _ <- queryUsers(bob) + _ <- followUser(bobUserId, bob, alice) + _ <- queryUsers(alice) + } yield succeed + } }