Skip to content

Commit f8a82ad

Browse files
authored
Add queryContractId to DAML Script (#7289)
* Add queryContractId to DAML Script I’ve switched between a few different naming options and in the end settled on this one: - `lookupContract`, not too bad but misses the connection to `query` - `queryCid`, we don’t abbreviate this in other places in our API so I don’t think we should here. - `queryContractId`, makes the connection clear and no abbreviation. We could also add `queryContractKey` later changelog_begin - [DAML Script] Add a `queryContractId` function for querying for a contract with the given identifier. changelog_end * Fix test changelog_begin changelog_end
1 parent fdb6945 commit f8a82ad

File tree

12 files changed

+225
-42
lines changed

12 files changed

+225
-42
lines changed

Diff for: compiler/damlc/tests/src/DA/Test/ScriptService.hs

+47
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,53 @@ main =
481481
matchRegex r "Tried to allocate a party that already exists: alice"
482482
expectScriptFailure rs (vr "duplicatePartyFromText") $ \r ->
483483
matchRegex r "Tried to allocate a party that already exists: bob"
484+
, testCase "lookup and fetch" $ do
485+
rs <-
486+
runScripts
487+
scriptService
488+
[ "module Test where"
489+
, "import DA.Assert"
490+
, "import Daml.Script"
491+
, "template T"
492+
, " with"
493+
, " owner : Party"
494+
, " observer : Party"
495+
, " where"
496+
, " signatory owner"
497+
, " observer observer"
498+
, "template Divulger"
499+
, " with"
500+
, " divulgee : Party"
501+
, " sig : Party"
502+
, " where"
503+
, " signatory divulgee"
504+
, " observer sig"
505+
, " nonconsuming choice Divulge : T"
506+
, " with cid : ContractId T"
507+
, " controller sig"
508+
, " do fetch cid"
509+
, "testQueryContractId = do"
510+
, " p1 <- allocateParty \"p1\""
511+
, " p2 <- allocateParty \"p2\""
512+
, " onlyP1 <- submit p1 $ createCmd (T p1 p1)"
513+
, " both <- submit p1 $ createCmd (T p1 p2)"
514+
, " divulger <- submit p2 $ createCmd (Divulger p2 p1)"
515+
, " optOnlyP1 <- queryContractId p1 onlyP1"
516+
, " optOnlyP1 === Some (T p1 p1)"
517+
, " optOnlyP1 <- queryContractId p2 onlyP1"
518+
, " optOnlyP1 === None"
519+
, " optBoth <- queryContractId p1 both"
520+
, " optBoth === Some (T p1 p2)"
521+
, " optBoth <- queryContractId p2 both"
522+
, " optBoth === Some (T p1 p2)"
523+
-- Divulged contracts should not be returned in lookups
524+
, " submit p1 $ exerciseCmd divulger (Divulge onlyP1)"
525+
, " optOnlyP1 <- queryContractId p2 onlyP1"
526+
, " optOnlyP1 === None"
527+
, " pure ()"
528+
]
529+
expectScriptSuccess rs (vr "testQueryContractId") $ \r ->
530+
matchRegex r "Active contracts: #2:0, #0:0, #1:0"
484531
]
485532
where
486533
scenarioConfig = SS.defaultScenarioServiceConfig {SS.cnfJvmOptions = ["-Xmx200M"]}

Diff for: daml-script/daml/Daml/Script.daml

+21
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module Daml.Script
77
, submit
88
, submitMustFail
99
, query
10+
, queryContractId
1011
, PartyIdHint (..)
1112
, ParticipantName (..)
1213
, PartyDetails(..)
@@ -87,6 +88,7 @@ data ScriptF a
8788
= Submit (SubmitCmd a)
8889
| SubmitMustFail (SubmitMustFailCmd a)
8990
| Query (QueryACS a)
91+
| QueryContractId (QueryContractIdPayload a)
9092
| AllocParty (AllocateParty a)
9193
| ListKnownParties (ListKnownPartiesPayload a)
9294
| GetTime (Time -> a)
@@ -105,6 +107,25 @@ data QueryACS a = QueryACS
105107
query : forall t. Template t => Party -> Script [(ContractId t, t)]
106108
query p = lift $ Free $ Query (QueryACS p (templateTypeRep @t) (pure . map (\(cid, tpl) -> (coerceContractId cid, fromSome $ fromAnyTemplate tpl))))
107109

110+
data QueryContractIdPayload a = QueryContractIdPayload
111+
{ party : Party
112+
, tplId : TemplateTypeRep
113+
, cid : ContractId ()
114+
, continue : Optional AnyTemplate -> a
115+
} deriving Functor
116+
117+
-- | Query for the contract with the given contract id.
118+
--
119+
-- Returns `None` if there is no active contract the party is a stakeholder on.
120+
-- This is semantically equivalent to calling `query`
121+
-- and filtering on the client side.
122+
queryContractId : forall t. Template t => Party -> ContractId t -> Script (Optional t)
123+
queryContractId p c = lift $ Free $ QueryContractId QueryContractIdPayload with
124+
party = p
125+
tplId = templateTypeRep @t
126+
cid = coerceContractId c
127+
continue = pure . fmap (fromSome . fromAnyTemplate)
128+
108129
data SetTimePayload a = SetTimePayload
109130
with
110131
time : Time

Diff for: daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/Converter.scala

+12-4
Original file line numberDiff line numberDiff line change
@@ -456,12 +456,11 @@ object Converter {
456456
entityName <- DottedName.fromString(id.entityName)
457457
} yield Identifier(packageId, QualifiedName(moduleName, entityName))
458458

459-
// Convert a Created event to a pair of (ContractId (), AnyTemplate)
460-
def fromCreated(
459+
// Convert an active contract to AnyTemplate
460+
def fromContract(
461461
translator: preprocessing.ValueTranslator,
462462
contract: ScriptLedgerClient.ActiveContract): Either[String, SValue] = {
463463
val anyTemplateTyCon = daInternalAny("AnyTemplate")
464-
val pairTyCon = daTypes("Tuple2")
465464
val tyCon = contract.templateId
466465
for {
467466
argSValue <- translator
@@ -470,7 +469,16 @@ object Converter {
470469
.map(
471470
err => s"Failure to translate value in create: $err"
472471
)
473-
anyTpl = record(anyTemplateTyCon, ("getAnyTemplate", SAny(TTyCon(tyCon), argSValue)))
472+
} yield record(anyTemplateTyCon, ("getAnyTemplate", SAny(TTyCon(tyCon), argSValue)))
473+
}
474+
475+
// Convert a Created event to a pair of (ContractId (), AnyTemplate)
476+
def fromCreated(
477+
translator: preprocessing.ValueTranslator,
478+
contract: ScriptLedgerClient.ActiveContract): Either[String, SValue] = {
479+
val pairTyCon = daTypes("Tuple2")
480+
for {
481+
anyTpl <- fromContract(translator, contract)
474482
} yield record(pairTyCon, ("_1", SContractId(contract.contractId)), ("_2", anyTpl))
475483
}
476484

Diff for: daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/LedgerInteraction.scala

+78
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ trait ScriptLedgerClient {
119119
implicit ec: ExecutionContext,
120120
mat: Materializer): Future[Seq[ScriptLedgerClient.ActiveContract]]
121121

122+
def queryContractId(party: SParty, templateId: Identifier, cid: ContractId)(
123+
implicit ec: ExecutionContext,
124+
mat: Materializer): Future[Option[ScriptLedgerClient.ActiveContract]]
125+
122126
def submit(
123127
party: SParty,
124128
commands: List[ScriptLedgerClient.Command],
@@ -180,6 +184,17 @@ class GrpcLedgerClient(val grpcClient: LedgerClient, val applicationId: Applicat
180184
})))
181185
}
182186

187+
override def queryContractId(party: SParty, templateId: Identifier, cid: ContractId)(
188+
implicit ec: ExecutionContext,
189+
mat: Materializer): Future[Option[ScriptLedgerClient.ActiveContract]] = {
190+
// We cannot do better than a linear search over query here.
191+
for {
192+
activeContracts <- query(party, templateId)
193+
} yield {
194+
activeContracts.find(c => c.contractId == cid)
195+
}
196+
}
197+
183198
override def submit(
184199
party: SParty,
185200
commands: List[ScriptLedgerClient.Command],
@@ -358,6 +373,26 @@ class IdeClient(val compiledPackages: CompiledPackages) extends ScriptLedgerClie
358373
})
359374
}
360375

376+
override def queryContractId(party: SParty, templateId: Identifier, cid: ContractId)(
377+
implicit ec: ExecutionContext,
378+
mat: Materializer): Future[Option[ScriptLedgerClient.ActiveContract]] = {
379+
scenarioRunner.ledger.lookupGlobalContract(
380+
view = ScenarioLedger.ParticipantView(party.value),
381+
effectiveAt = scenarioRunner.ledger.currentTime,
382+
cid) match {
383+
case ScenarioLedger.LookupOk(_, Value.ContractInst(_, arg, _), stakeholders)
384+
if stakeholders.contains(party.value) =>
385+
Future.successful(Some(ScriptLedgerClient.ActiveContract(templateId, cid, arg.value)))
386+
case _ =>
387+
// Note that contrary to `fetch` in a scenario, we do not
388+
// abort on any of the error cases. This makes sense if you
389+
// consider this a wrapper around the ACS endpoint where
390+
// we cannot differentiate between visibility errors
391+
// and the contract not being active.
392+
Future.successful(None)
393+
}
394+
}
395+
361396
// Translate from a ledger command to an Update expression
362397
// corresponding to the same command.
363398
private def translateCommand(cmd: ScriptLedgerClient.Command): speedy.Command = {
@@ -635,6 +670,38 @@ class JsonLedgerClient(
635670
parsedResults
636671
}
637672
}
673+
override def queryContractId(party: SParty, templateId: Identifier, cid: ContractId)(
674+
implicit ec: ExecutionContext,
675+
mat: Materializer) = {
676+
val req = HttpRequest(
677+
method = HttpMethods.POST,
678+
uri = uri.withPath(uri.path./("v1")./("fetch")),
679+
entity = HttpEntity(
680+
ContentTypes.`application/json`,
681+
JsonLedgerClient.FetchArgs(cid).toJson.prettyPrint),
682+
headers = List(Authorization(OAuth2BearerToken(token.value)))
683+
)
684+
for {
685+
() <- validateTokenParty(party, "queryContractId")
686+
resp <- Http().singleRequest(req)
687+
fetchResponse <- if (resp.status.isSuccess) {
688+
Unmarshal(resp.entity).to[JsonLedgerClient.FetchResponse]
689+
} else {
690+
getResponseDataBytes(resp).flatMap {
691+
case body => Future.failed(new RuntimeException(s"Failed to query ledger: $resp, $body"))
692+
}
693+
}
694+
} yield {
695+
val ctx = templateId.qualifiedName
696+
val ifaceType = Converter.toIfaceType(ctx, TTyCon(templateId)).right.get
697+
fetchResponse.result.map(r => {
698+
val payload = r.payload.convertTo[Value[ContractId]](
699+
LfValueCodec.apiValueJsonReader(ifaceType, damlLfTypeLookup(_)))
700+
val cid = ContractId.assertFromString(r.contractId)
701+
ScriptLedgerClient.ActiveContract(templateId, cid, payload)
702+
})
703+
}
704+
}
638705
override def submit(
639706
party: SParty,
640707
commands: List[ScriptLedgerClient.Command],
@@ -867,6 +934,8 @@ object JsonLedgerClient {
867934
final case class QueryArgs(templateId: Identifier)
868935
final case class QueryResponse(results: List[ActiveContract])
869936
final case class ActiveContract(contractId: String, payload: JsValue)
937+
final case class FetchArgs(contractId: ContractId)
938+
final case class FetchResponse(result: Option[ActiveContract])
870939

871940
final case class CreateArgs(templateId: Identifier, payload: JsValue)
872941
final case class CreateResponse(contractId: String)
@@ -911,6 +980,15 @@ object JsonLedgerClient {
911980
case _ => deserializationError(s"Could not parse QueryResponse: $v")
912981
}
913982
}
983+
implicit val fetchWriter: JsonWriter[FetchArgs] = args =>
984+
JsObject("contractId" -> args.contractId.coid.toString.toJson)
985+
implicit val fetchReader: RootJsonReader[FetchResponse] = v =>
986+
v.asJsObject.getFields("result") match {
987+
case Seq(JsNull) => FetchResponse(None)
988+
case Seq(r) => FetchResponse(Some(r.convertTo[ActiveContract]))
989+
case _ => deserializationError(s"Could not parse FetchResponse: $v")
990+
}
991+
914992
implicit val activeContractReader: RootJsonReader[ActiveContract] = v => {
915993
v.asJsObject.getFields("contractId", "payload") match {
916994
case Seq(JsString(s), v) => ActiveContract(s, v)

Diff for: daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/Runner.scala

+20
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,26 @@ class Runner(compiledPackages: CompiledPackages, script: Script.Action, timeMode
656656
new ConverterException(s"Expected record with 2 fields but got $v"))
657657
}
658658
}
659+
case SVariant(_, "QueryContractId", _, v) => {
660+
v match {
661+
case SRecord(_, _, vals) if vals.size == 4 => {
662+
val continue = vals.get(3)
663+
for {
664+
party <- Converter.toFuture(Converter.toParty(vals.get(0)))
665+
tplId <- Converter.toFuture(Converter.typeRepToIdentifier(vals.get(1)))
666+
cid <- Converter.toFuture(Converter.toContractId(vals.get(2)))
667+
client <- Converter.toFuture(clients.getPartyParticipant(Party(party.value)))
668+
optR <- client.queryContractId(party, tplId, cid)
669+
optR <- Converter.toFuture(
670+
optR.traverse(Converter.fromContract(valueTranslator, _)))
671+
v <- run(SEApp(SEValue(continue), Array(SEValue(SOptional(optR)))))
672+
} yield v
673+
}
674+
case _ =>
675+
Future.failed(
676+
new ConverterException(s"Expected record with 4 fields but got $v"))
677+
}
678+
}
659679
case _ =>
660680
Future.failed(
661681
new ConverterException(s"Expected Submit, Query or AllocParty but got $v"))

Diff for: daml-script/test/daml-script-test-runner.sh

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ ScriptTest:sleepTest SUCCESS
5757
ScriptExample:initializeFixed SUCCESS
5858
ScriptTest:testStack SUCCESS
5959
ScriptTest:testMaxInboundMessageSize SUCCESS
60+
ScriptTest:testQueryContractId SUCCESS
6061
EOF
6162
)"
6263

Diff for: daml-script/test/daml/ScriptTest.daml

+12
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,15 @@ testSetTime : Script (Time, Time) = do
269269
setTime (time (date 2000 Feb 2) 0 1 2)
270270
t1 <- getTime
271271
pure (t0, t1)
272+
273+
testQueryContractId : Script () = do
274+
p <- allocateParty "p"
275+
jsonQueryContractId p
276+
277+
jsonQueryContractId p = do
278+
cid <- submit p do createCmd (C p 42)
279+
optR <- queryContractId p cid
280+
optR === Some (C p 42)
281+
submit p do archiveCmd cid
282+
optR <- queryContractId p cid
283+
optR === None

Diff for: daml-script/test/src/com/digitalasset/daml/lf/engine/script/test/JsonApiIt.scala

+8
Original file line numberDiff line numberDiff line change
@@ -361,5 +361,13 @@ final class JsonApiIt
361361
assert(ex.toString.contains("Cannot resolve template ID"))
362362
}
363363
}
364+
"queryContractId" in {
365+
for {
366+
clients <- getClients()
367+
result <- run(clients, QualifiedName.assertFromString("ScriptTest:jsonQueryContractId"))
368+
} yield {
369+
assert(result == SUnit)
370+
}
371+
}
364372
}
365373
}

Diff for: daml-script/test/src/com/digitalasset/daml/lf/engine/script/test/SingleParticipant.scala

+17
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,22 @@ case class TestContractId(dar: Dar[(PackageId, Package)], runner: TestRunner) {
408408
}
409409
}
410410

411+
case class TestLookupFetch(dar: Dar[(PackageId, Package)], runner: TestRunner) {
412+
val scriptId =
413+
Identifier(dar.main._1, QualifiedName.assertFromString("ScriptTest:testQueryContractId"))
414+
def runTests(): Unit = {
415+
runner.genericTest(
416+
"testQueryContractId",
417+
scriptId,
418+
None, {
419+
// We test with assertions in the test
420+
case SUnit => Right(())
421+
case v => Left(s"Expected SUnit but got $v")
422+
}
423+
)
424+
}
425+
}
426+
411427
object SingleParticipant {
412428

413429
private val configParser = new scopt.OptionParser[Config]("daml_script_test") {
@@ -512,6 +528,7 @@ object SingleParticipant {
512528
val devRunner =
513529
new TestRunner(participantParams, devDar, config.wallclockTime, config.rootCa)
514530
if (!config.auth) {
531+
TestLookupFetch(dar, runner).runTests()
515532
TraceOrder(dar, runner).runTests()
516533
Test0(dar, runner).runTests()
517534
Test1(dar, runner).runTests()

Diff for: docs/source/app-dev/bindings-java/quickstart/template-root/daml/Tests/Helper.daml

-22
This file was deleted.

0 commit comments

Comments
 (0)