diff --git a/sdk/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltinFun.scala b/sdk/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltinFun.scala index 4f75f40ab6cb..110cee81d901 100644 --- a/sdk/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltinFun.scala +++ b/sdk/daml-lf/interpreter/src/main/scala/com/digitalasset/daml/lf/speedy/SBuiltinFun.scala @@ -2287,59 +2287,76 @@ private[lf] object SBuiltinFun { machine: UpdateMachine, dstTmplId: TypeConName, coid: V.ContractId, - )(f: SValue => Control[Question.Update]): Control[Question.Update] = - machine.getIfLocalContract(coid) match { - case Some((srcTmplId, templateArg)) => - ensureContractActive(machine, coid, srcTmplId) { - if (srcTmplId.qualifiedName != dstTmplId.qualifiedName) - Control.Error( - IE.WronglyTypedContract(coid, dstTmplId, srcTmplId) - ) - else f(templateArg) - } - case None => - machine.lookupContract(coid) { case V.ContractInstance(_, _, srcTmplId, coinstArg) => - if (srcTmplId.qualifiedName != dstTmplId.qualifiedName) - Control.Error( - IE.WronglyTypedContract(coid, dstTmplId, srcTmplId) - ) - else - machine.ensurePackageIsLoaded( - dstTmplId.packageId, - language.Reference.Template(dstTmplId), - ) { () => - importValue(machine, dstTmplId, coinstArg) { templateArg => - getContractInfo( - machine, - coid, - dstTmplId, - templateArg, - allowCatchingContractInfoErrors = false, - ) { contract => - ensureContractActive(machine, coid, contract.templateId) { - - machine.checkContractVisibility(coid, contract) - machine.enforceLimitAddInputContract() - machine.enforceLimitSignatoriesAndObservers(coid, contract) - - // In Validation mode, we always call validateContractInfo - // In Submission mode, we only call validateContractInfo when src != dest - val needValidationCall: Boolean = - machine.validating || srcTmplId.packageId != dstTmplId.packageId - if (needValidationCall) { - validateContractInfo(machine, coid, srcTmplId, contract) { () => - f(contract.value) - } - } else { - f(contract.value) - } + )(f: SValue => Control[Question.Update]): Control[Question.Update] = { + def importContract(coinst: V.ContractInstance) = { + val V.ContractInstance(_, _, srcTmplId, coinstArg) = coinst + if (srcTmplId.qualifiedName != dstTmplId.qualifiedName) + Control.Error( + IE.WronglyTypedContract(coid, dstTmplId, srcTmplId) + ) + else + machine.ensurePackageIsLoaded( + dstTmplId.packageId, + language.Reference.Template(dstTmplId), + ) { () => + importValue(machine, dstTmplId, coinstArg) { templateArg => + getContractInfo( + machine, + coid, + dstTmplId, + templateArg, + allowCatchingContractInfoErrors = false, + ) { contract => + ensureContractActive(machine, coid, contract.templateId) { + + machine.checkContractVisibility(coid, contract) + machine.enforceLimitAddInputContract() + machine.enforceLimitSignatoriesAndObservers(coid, contract) + + // In Validation mode, we always call validateContractInfo + // In Submission mode, we only call validateContractInfo when src != dest + val needValidationCall: Boolean = + machine.validating || srcTmplId.packageId != dstTmplId.packageId + if (needValidationCall) { + validateContractInfo(machine, coid, srcTmplId, contract) { () => + f(contract.value) } + } else { + f(contract.value) } } } + } } } + machine.getIfLocalContract(coid) match { + case Some((srcTmplId, templateArg)) => + ensureContractActive(machine, coid, srcTmplId) { + getContractInfo( + machine, + coid, + srcTmplId, + templateArg, + allowCatchingContractInfoErrors = false, + ) { contract => + if (srcTmplId == dstTmplId) f(templateArg) + else + importContract( + V.ContractInstance( + contract.packageName, + contract.packageVersion, + srcTmplId, + contract.arg, + ) + ) + } + } + case None => + machine.lookupContract(coid)(importContract) + } + } + /** A version of [[fetchTemplate]] without a destination template type. The template type of the contract on ledger * is used for importing its value, and is returned alongside the value. */ diff --git a/sdk/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ExceptionTest.scala b/sdk/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ExceptionTest.scala index 014e6079a1a6..32e6f1220d8b 100644 --- a/sdk/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ExceptionTest.scala +++ b/sdk/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/ExceptionTest.scala @@ -815,7 +815,7 @@ class ExceptionTest(majorLanguageVersion: LanguageMajorVersion) }; val mkParty : Text -> Party = \(t:Text) -> case TEXT_TO_PARTY t of None -> ERROR @Party "none" | Some x -> x; - val alice : Party = Mod:mkParty "alice"; + val alice : Party = Mod:mkParty "Alice"; } """ (parserParameters.copy(defaultPackageId = commonDefsPkgId)) @@ -826,11 +826,8 @@ class ExceptionTest(majorLanguageVersion: LanguageMajorVersion) */ abstract class TemplateGenerator(val templateName: String) { def precondition = """True""" - def signatories = s"""Cons @Party [Mod:${templateName} {p} this] (Nil @Party)""" - def observers = """Nil @Party""" - def key = s""" | '$commonDefsPkgId':Mod:Key { @@ -943,7 +940,7 @@ class ExceptionTest(majorLanguageVersion: LanguageMajorVersion) * - When an instance of [[templateName]] is fetched/exercised by id/key/interface, exceptions thrown when * evaluating its metadata cannot be caught. */ - def tests(pkgId: Ref.PackageId, templateName: String): String = { + def globalContractTests(pkgId: Ref.PackageId, templateName: String): String = { val tplQualifiedName = s"'$pkgId':Mod:$templateName" s""" | // Checks that the error thrown when creating a $templateName instance can be caught. @@ -957,7 +954,7 @@ class ExceptionTest(majorLanguageVersion: LanguageMajorVersion) | | // Tries to catch the error thrown by the contract info of $templateName when exercising a choice on | // it, should fail to do so. - | val exercise${templateName}AndCatchError: (ContractId $tplQualifiedName) -> Update Text = + | val exercise${templateName}AndCatchErrorGlobal: (ContractId $tplQualifiedName) -> Update Text = | \\(cid: ContractId $tplQualifiedName) -> | try @Text | exercise @$tplQualifiedName SomeChoice cid () @@ -966,7 +963,7 @@ class ExceptionTest(majorLanguageVersion: LanguageMajorVersion) | | // Tries to catch the error thrown by the contract info of a $templateName contract when fetching it, | // should fail to do so. - | val fetch${templateName}AndCatchError: (ContractId $tplQualifiedName) -> Update Text = + | val fetch${templateName}AndCatchErrorGlobal: (ContractId $tplQualifiedName) -> Update Text = | \\(cid: ContractId $tplQualifiedName) -> | try @Text | ubind _:$tplQualifiedName <- fetch_template @$tplQualifiedName cid @@ -976,7 +973,7 @@ class ExceptionTest(majorLanguageVersion: LanguageMajorVersion) | | // Tries to catch the error thrown by the contract info of a $templateName contract when fetching it | // by interface, should fail to do so. - | val fetch${templateName}ByInterfaceAndCatchError: (ContractId $tplQualifiedName) -> Update Text = + | val fetch${templateName}ByInterfaceAndCatchErrorGlobal: (ContractId $tplQualifiedName) -> Update Text = | \\(cid: ContractId $tplQualifiedName) -> | try @Text | ubind _:'$commonDefsPkgId':Mod:Iface <- @@ -988,7 +985,7 @@ class ExceptionTest(majorLanguageVersion: LanguageMajorVersion) | | // Tries to catch the error thrown by the contract info of a $templateName contract when fetching it | // by key, should fail to do so. - | val fetch${templateName}ByKeyAndCatchError: '$commonDefsPkgId':Mod:Key -> Update Text = + | val fetch${templateName}ByKeyAndCatchErrorGlobal: '$commonDefsPkgId':Mod:Key -> Update Text = | \\(key: '$commonDefsPkgId':Mod:Key) -> | try @Text | ubind _:$tuple2TyCon (ContractId $tplQualifiedName) $tplQualifiedName <- @@ -999,7 +996,7 @@ class ExceptionTest(majorLanguageVersion: LanguageMajorVersion) | | // Tries to catch the error thrown by the contract info of a $templateName contract when looking it up | // by key, should fail to do so. - | val lookUp${templateName}ByKeyAndCatchError: '$commonDefsPkgId':Mod:Key -> Update Text = + | val lookUp${templateName}ByKeyAndCatchErrorGlobal: '$commonDefsPkgId':Mod:Key -> Update Text = | \\(key: '$commonDefsPkgId':Mod:Key) -> | try @Text | ubind _:Option (ContractId $tplQualifiedName) <- @@ -1010,11 +1007,102 @@ class ExceptionTest(majorLanguageVersion: LanguageMajorVersion) |""".stripMargin } - def dynamicChoiceTests(templateName: String): String = { + /** Generates a series of expressions meant to test that when a locally created v1 instance of [[templateName]] is + * fetched/exercised by id/key/interface as a v2 exceptions thrown when evaluating its metadata cannot be caught. + */ + def localContractTests( + v1PkgId: Ref.PackageId, + v2PkgId: Ref.PackageId, + templateName: String, + ): String = { + val v1TplQualifiedName = s"'$v1PkgId':Mod:$templateName" + val v2TplQualifiedName = s"'$v2PkgId':Mod:$templateName" + s""" + | // Tries to catch the error thrown by the contract info of $templateName when exercising a choice on + | // it, should fail to do so. + | val exercise${templateName}AndCatchErrorLocal: Unit -> Update Text = + | \\(_:Unit) -> + | ubind cid: ContractId $v1TplQualifiedName <- + | create @$v1TplQualifiedName ($v1TplQualifiedName { p = '$commonDefsPkgId':Mod:alice }) + | in try @Text + | exercise + | @$v2TplQualifiedName + | SomeChoice + | (COERCE_CONTRACT_ID @$v1TplQualifiedName @$v2TplQualifiedName cid) + | () + | catch + | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | + | // Tries to catch the error thrown by the contract info of a $templateName contract when fetching it, + | // should fail to do so. + | val fetch${templateName}AndCatchErrorLocal: Unit -> Update Text = + | \\(_:Unit) -> + | ubind cid: ContractId $v1TplQualifiedName <- + | create @$v1TplQualifiedName ($v1TplQualifiedName { p = '$commonDefsPkgId':Mod:alice }) + | in try @Text + | ubind _:$v2TplQualifiedName <- + | fetch_template @$v2TplQualifiedName + | (COERCE_CONTRACT_ID @$v1TplQualifiedName @$v2TplQualifiedName cid) + | in upure @Text "unexpected: contract was fetched" + | catch + | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | + | // Tries to catch the error thrown by the contract info of a $templateName contract when fetching it + | // by interface, should fail to do so. + | val fetch${templateName}ByInterfaceAndCatchErrorLocal: Unit -> Update Text = + | \\(_:Unit) -> + | ubind cid: ContractId $v1TplQualifiedName <- + | create @$v1TplQualifiedName ($v1TplQualifiedName { p = '$commonDefsPkgId':Mod:alice }) + | in try @Text + | ubind _:'$commonDefsPkgId':Mod:Iface <- + | fetch_interface @'$commonDefsPkgId':Mod:Iface + | (COERCE_CONTRACT_ID @$v1TplQualifiedName @'$commonDefsPkgId':Mod:Iface cid) + | in upure @Text "unexpected: contract was fetched by interface" + | catch + | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | + | // Tries to catch the error thrown by the contract info of a $templateName contract when fetching it + | // by key, should fail to do so. + | val fetch${templateName}ByKeyAndCatchErrorLocal: Unit -> Update Text = + | \\(_:Unit) -> + | ubind cid: ContractId $v1TplQualifiedName <- + | create @$v1TplQualifiedName ($v1TplQualifiedName { p = '$commonDefsPkgId':Mod:alice }) + | in try @Text + | ubind _:$tuple2TyCon (ContractId $v2TplQualifiedName) $v2TplQualifiedName <- + | fetch_by_key + | @$v2TplQualifiedName + | ('$commonDefsPkgId':Mod:Key { + | label = "test-key", + | maintainers = (Cons @Party ['$commonDefsPkgId':Mod:alice] (Nil @Party)) }) + | in upure @Text "unexpected: contract was fetched by key" + | catch + | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + | + | // Tries to catch the error thrown by the contract info of a $templateName contract when looking it up + | // by key, should fail to do so. + | val lookUp${templateName}ByKeyAndCatchErrorLocal: Unit -> Update Text = + | \\(_:Unit) -> + | ubind cid: ContractId $v1TplQualifiedName <- + | create @$v1TplQualifiedName ($v1TplQualifiedName { p = '$commonDefsPkgId':Mod:alice }) + | in try @Text + | ubind _:Option (ContractId $v2TplQualifiedName) <- + | lookup_by_key + | @$v2TplQualifiedName + | ('$commonDefsPkgId':Mod:Key { + | label = "test-key", + | maintainers = (Cons @Party ['$commonDefsPkgId':Mod:alice] (Nil @Party)) }) + | in upure @Text "unexpected: contract was looked up by key" + | catch + | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + |""".stripMargin + } + + def dynamicChoiceTestsGlobal(templateName: String): String = { s""" | // Tries to catch the error thrown by the dynamic exercise of a $templateName choice when fetching it | // by interface, should fail to do so. - | val exercise${templateName}ByInterfaceAndCatchError: (ContractId '$commonDefsPkgId':Mod:Iface) -> Update Text = + | val exercise${templateName}ByInterfaceAndCatchErrorGlobal: + | (ContractId '$commonDefsPkgId':Mod:Iface) -> Update Text = | \\(cid: ContractId '$commonDefsPkgId':Mod:Iface) -> | try @Text | exercise_interface @'$commonDefsPkgId':Mod:Iface MyChoice cid () @@ -1023,18 +1111,47 @@ class ExceptionTest(majorLanguageVersion: LanguageMajorVersion) """.stripMargin } + def dynamicChoiceTestsLocal(v1PkgId: Ref.PackageId, templateName: String): String = { + val v1TplQualifiedName = s"'$v1PkgId':Mod:$templateName" + s""" + | // Tries to catch the error thrown by the dynamic exercise of a $templateName choice when fetching it + | // by interface, should fail to do so. + | val exercise${templateName}ByInterfaceAndCatchErrorLocal: Unit -> Update Text = + | \\(_:Unit) -> + | ubind cid: ContractId $v1TplQualifiedName <- + | create @$v1TplQualifiedName ($v1TplQualifiedName { p = '$commonDefsPkgId':Mod:alice }) + | in try @Text + | exercise_interface @'$commonDefsPkgId':Mod:Iface + | MyChoice + | (COERCE_CONTRACT_ID @$v1TplQualifiedName @'$commonDefsPkgId':Mod:Iface cid) + | () + | catch + | e -> Some @(Update Text) (upure @Text "unexpected: some exception was caught"); + """.stripMargin + } + val metadataTestsPkgId = Ref.PackageId.assertFromString("-metadata-tests-id-") val metadataTestsParserParams = parserParameters.copy(defaultPackageId = metadataTestsPkgId) val metadataTestsPkg = p"""metadata ( '-metadata-tests-' : '1.0.0' ) module Mod { - ${tests(templateDefsV2PkgId, "Precondition")} - ${tests(templateDefsV2PkgId, "Signatories")} - ${tests(templateDefsV2PkgId, "Observers")} - ${tests(templateDefsV2PkgId, "Key")} - ${tests(templateDefsV2PkgId, "Maintainers")} - ${dynamicChoiceTests("ChoiceControllers")} - ${dynamicChoiceTests("ChoiceObservers")} + ${globalContractTests(templateDefsV2PkgId, "Precondition")} + ${globalContractTests(templateDefsV2PkgId, "Signatories")} + ${globalContractTests(templateDefsV2PkgId, "Observers")} + ${globalContractTests(templateDefsV2PkgId, "Key")} + ${globalContractTests(templateDefsV2PkgId, "Maintainers")} + + ${localContractTests(templateDefsV1PkgId, templateDefsV2PkgId, "Precondition")} + ${localContractTests(templateDefsV1PkgId, templateDefsV2PkgId, "Signatories")} + ${localContractTests(templateDefsV1PkgId, templateDefsV2PkgId, "Observers")} + ${localContractTests(templateDefsV1PkgId, templateDefsV2PkgId, "Key")} + ${localContractTests(templateDefsV1PkgId, templateDefsV2PkgId, "Maintainers")} + + ${dynamicChoiceTestsGlobal("ChoiceControllers")} + ${dynamicChoiceTestsGlobal("ChoiceObservers")} + + ${dynamicChoiceTestsLocal(templateDefsV1PkgId, "ChoiceControllers")} + ${dynamicChoiceTestsLocal(templateDefsV1PkgId, "ChoiceObservers")} } """ (metadataTestsParserParams) @@ -1071,7 +1188,8 @@ class ExceptionTest(majorLanguageVersion: LanguageMajorVersion) s"exceptions thrown by ${test.templateName} cannot be caught when fetched or exercised" in { val alice = Ref.Party.assertFromString("Alice") - val templateId = Ref.Identifier.assertFromString(s"-pkgId-:Mod:${test.templateName}") + val templateId = + Ref.Identifier.assertFromString(s"-template-defs-v1-id-:Mod:${test.templateName}") val cid = Value.ContractId.V1(crypto.Hash.hashPrivateKey("abc")) val key = SValue.SRecord( templateId, @@ -1095,25 +1213,49 @@ class ExceptionTest(majorLanguageVersion: LanguageMajorVersion) Table[Expr, SValue]( ("expression", "arg"), ( - e"Mod:exercise${test.templateName}AndCatchError" (metadataTestsParserParams), + e"Mod:exercise${test.templateName}AndCatchErrorGlobal" (metadataTestsParserParams), SContractId(cid), ), ( - e"Mod:fetch${test.templateName}AndCatchError" (metadataTestsParserParams), + e"Mod:exercise${test.templateName}AndCatchErrorLocal" (metadataTestsParserParams), + SUnit, + ), + ( + e"Mod:fetch${test.templateName}AndCatchErrorGlobal" (metadataTestsParserParams), SContractId(cid), ), ( - e"Mod:fetch${test.templateName}ByInterfaceAndCatchError" (metadataTestsParserParams), + e"Mod:fetch${test.templateName}AndCatchErrorLocal" (metadataTestsParserParams), + SUnit, + ), + ( + e"Mod:fetch${test.templateName}ByInterfaceAndCatchErrorGlobal" ( + metadataTestsParserParams + ), SContractId(cid), ), ( - e"Mod:fetch${test.templateName}ByKeyAndCatchError" (metadataTestsParserParams), + e"Mod:fetch${test.templateName}ByInterfaceAndCatchErrorLocal" ( + metadataTestsParserParams + ), + SUnit, + ), + ( + e"Mod:fetch${test.templateName}ByKeyAndCatchErrorGlobal" (metadataTestsParserParams), key, ), ( - e"Mod:lookUp${test.templateName}ByKeyAndCatchError" (metadataTestsParserParams), + e"Mod:fetch${test.templateName}ByKeyAndCatchErrorLocal" (metadataTestsParserParams), + SUnit, + ), + ( + e"Mod:lookUp${test.templateName}ByKeyAndCatchErrorGlobal" (metadataTestsParserParams), key, ), + ( + e"Mod:lookUp${test.templateName}ByKeyAndCatchErrorLocal" (metadataTestsParserParams), + SUnit, + ), ) } @@ -1168,38 +1310,58 @@ class ExceptionTest(majorLanguageVersion: LanguageMajorVersion) val alice = Ref.Party.assertFromString("Alice") val cid = Value.ContractId.V1(crypto.Hash.hashPrivateKey("abc")) - inside { - runUpdateApp( - compiledPackages, - packageResolution = Map( - templateDefsPkgName -> templateDefsV2PkgId + val testCases = { + Table[Expr, SValue]( + ("expression", "arg"), + ( + e"Mod:exercise${test.templateName}ByInterfaceAndCatchErrorGlobal" ( + metadataTestsParserParams + ), + SContractId(cid), ), - e"Mod:exercise${test.templateName}ByInterfaceAndCatchError" (metadataTestsParserParams), - Array(SContractId(cid)), - getContract = Map( - cid -> Versioned( - version = TransactionVersion.StableVersions.max, - Value.ContractInstance( - packageName = metadataTestsPkg.pkgName, - template = t"Mod:${test.templateName}" (templateDefsV1ParserParams) - .asInstanceOf[Ast.TTyCon] - .tycon, - arg = Value.ValueRecord(None, ImmArray(None -> Value.ValueParty(alice))), - ), - ) + ( + e"Mod:exercise${test.templateName}ByInterfaceAndCatchErrorLocal" ( + metadataTestsParserParams + ), + SUnit, ), - getKey = PartialFunction.empty, ) - } { - case Left( - SError.SErrorDamlException( - IE.UnhandledException( - _, - Value.ValueRecord(_, ImmArray((_, Value.ValueText(msg)))), - ) + } + + forEvery(testCases) { (expr, arg) => + inside { + runUpdateApp( + compiledPackages, + packageResolution = Map( + templateDefsPkgName -> templateDefsV2PkgId + ), + expr, + Array(arg), + getContract = Map( + cid -> Versioned( + version = TransactionVersion.StableVersions.max, + Value.ContractInstance( + packageName = metadataTestsPkg.pkgName, + template = t"Mod:${test.templateName}" (templateDefsV1ParserParams) + .asInstanceOf[Ast.TTyCon] + .tycon, + arg = Value.ValueRecord(None, ImmArray(None -> Value.ValueParty(alice))), + ), ) - ) => - msg shouldBe test.templateName + ), + getKey = PartialFunction.empty, + ) + } { + case Left( + SError.SErrorDamlException( + IE.UnhandledException( + _, + Value.ValueRecord(_, ImmArray((_, Value.ValueText(msg)))), + ) + ) + ) => + msg shouldBe test.templateName + } } } } diff --git a/sdk/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/UpgradeTest.scala b/sdk/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/UpgradeTest.scala index 3ba16eece8d8..b3c298cddb9b 100644 --- a/sdk/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/UpgradeTest.scala +++ b/sdk/daml-lf/interpreter/src/test/scala/com/digitalasset/daml/lf/speedy/UpgradeTest.scala @@ -42,6 +42,26 @@ class UpgradeTest(majorLanguageVersion: LanguageMajorVersion) ): ParserParameters[this.type] = ParserParameters(pkgId, languageVersion = majorLanguageVersion.dev) + val ifacePkgId = Ref.PackageId.assertFromString("-iface-") + private lazy val ifacePkg = { + implicit def pkgId: Ref.PackageId = ifacePkgId + p""" metadata ( '-iface-' : '1.0.0' ) + module M { + + record @serializable MyUnit = {}; + interface (this : Iface) = { + viewtype M:MyUnit; + method myChoice : Text; + + choice @nonConsuming MyChoice (self) (ctl: Party): Text + , controllers (Cons @Party [ctl] Nil @Party) + , observers (Nil @Party) + to upure @Text (call_method @M:Iface myChoice this); + }; + } + """ + } + val pkgId1 = Ref.PackageId.assertFromString("-pkg1-") private lazy val pkg1 = { implicit def pkgId: Ref.PackageId = pkgId1 @@ -53,9 +73,23 @@ class UpgradeTest(majorLanguageVersion: LanguageMajorVersion) precondition True; signatories M:mkList (M:T {sig} this) (None @Party); observers M:mkList (M:T {obs} this) (None @Party); + implements '-iface-':M:Iface { + view = '-iface-':M:MyUnit {}; + method myChoice = "myChoice v1"; + }; key @Party (M:T {sig} this) (\ (p: Party) -> Cons @Party [p] Nil @Party); }; + val mkParty : Text -> Party = + \(t:Text) -> + case TEXT_TO_PARTY t of + None -> ERROR @Party "none" + | Some x -> x; + + val do_create: Text -> Text -> Int64 -> Update (ContractId M:T) = + \(sig: Text) -> \(obs: Text) -> \(n: Int64) -> + create @M:T M:T { sig = M:mkParty sig, obs = M:mkParty obs, aNumber = n }; + val do_fetch: ContractId M:T -> Update M:T = \(cId: ContractId M:T) -> fetch_template @M:T cId; @@ -73,7 +107,7 @@ class UpgradeTest(majorLanguageVersion: LanguageMajorVersion) val pkgId2: Ref.PackageId = Ref.PackageId.assertFromString("-pkg2-") private lazy val pkg2 = { - // same signatures as pkg1 + // adds a choice to T implicit def pkgId: Ref.PackageId = pkgId2 p""" metadata ( '-upgrade-test-' : '2.0.0' ) module M { @@ -83,6 +117,14 @@ class UpgradeTest(majorLanguageVersion: LanguageMajorVersion) precondition True; signatories '-pkg1-':M:mkList (M:T {sig} this) (None @Party); observers '-pkg1-':M:mkList (M:T {obs} this) (None @Party); + choice NoOp (self) (arg: Unit) : Unit, + controllers Cons @Party [M:T {sig} this] Nil @Party, + observers Nil @Party + to upure @Unit (); + implements '-iface-':M:Iface { + view = '-iface-':M:MyUnit {}; + method myChoice = "myChoice v2"; + }; key @Party (M:T {sig} this) (\ (p: Party) -> Cons @Party [p] Nil @Party); }; @@ -105,6 +147,10 @@ class UpgradeTest(majorLanguageVersion: LanguageMajorVersion) precondition True; signatories '-pkg1-':M:mkList (M:T {sig} this) (M:T {optSig} this); observers '-pkg1-':M:mkList (M:T {obs} this) (None @Party); + choice NoOp (self) (arg: Unit) : Unit, + controllers Cons @Party [M:T {sig} this] Nil @Party, + observers Nil @Party + to upure @Unit (); key @Party (M:T {sig} this) (\ (p: Party) -> Cons @Party [p] Nil @Party); }; @@ -128,6 +174,10 @@ class UpgradeTest(majorLanguageVersion: LanguageMajorVersion) precondition True; signatories '-pkg1-':M:mkList (M:T {obs} this) (None @Party); observers '-pkg1-':M:mkList (M:T {sig} this) (None @Party); + choice NoOp (self) (u: Unit) : Unit, + controllers '-pkg1-':M:mkList (M:T {sig} this) (None @Party), + observers Nil @Party + to upure @Unit (); key @Party (M:T {obs} this) (\ (p: Party) -> Cons @Party [p] Nil @Party); }; @@ -152,7 +202,13 @@ class UpgradeTest(majorLanguageVersion: LanguageMajorVersion) private lazy val pkgs = PureCompiledPackages.assertBuild( - Map(pkgId1 -> pkg1, pkgId2 -> pkg2, pkgId3 -> pkg3, pkgId4 -> pkg4), + Map( + ifacePkgId -> ifacePkg, + pkgId1 -> pkg1, + pkgId2 -> pkg2, + pkgId3 -> pkg3, + pkgId4 -> pkg4, + ), Compiler.Config.Dev(majorLanguageVersion), ) @@ -163,6 +219,31 @@ class UpgradeTest(majorLanguageVersion: LanguageMajorVersion) type Success = (SValue, Value, List[UpgradeVerificationRequest]) + def go( + e: Expr, + packageResolution: Map[Ref.PackageName, Ref.PackageId] = Map.empty, + ): Either[SError, Success] = { + + val sexprToEval: SExpr = pkgs.compiler.unsafeCompile(e) + + implicit def logContext: LoggingContext = LoggingContext.ForTesting + val seed = crypto.Hash.hashPrivateKey("seed") + val machine = Speedy.Machine.fromUpdateSExpr( + pkgs, + seed, + sexprToEval, + Set(alice, bob), + packageResolution = packageResolution, + ) + + SpeedyTestLib + .runCollectRequests(machine) + .map { case (sv, uvs) => // ignoring any AuthRequest + val v = sv.toNormalizedValue(VDev) + (sv, v, uvs) + } + } + // The given contractValue is wrapped as a contract available for ledger-fetch def go(e: Expr, contract: ContractInstance): Either[SError, Success] = { @@ -232,6 +313,9 @@ class UpgradeTest(majorLanguageVersion: LanguageMajorVersion) val v1_key = GlobalKeyWithMaintainers.assertBuild(i"'-pkg1-':M:T", ValueParty(alice), Set(alice), pkgName) + val v2_key = + GlobalKeyWithMaintainers.assertBuild(i"'-pkg2-':M:T", ValueParty(alice), Set(alice), pkgName) + "upgrade attempted" - { "missing optional field -- None is manufactured" in { @@ -403,6 +487,92 @@ class UpgradeTest(majorLanguageVersion: LanguageMajorVersion) ) res shouldBe a[Right[_, _]] } + + "be able to fetch a locally created contract using different versions" in { + val res = go( + e"""ubind + cid: ContractId '-pkg1-':M:T <- '-pkg1-':M:do_create "alice" "bob" 100; + _: '-pkg2-':M:T <- fetch_template @'-pkg2-':M:T cid + in upure @(ContractId '-pkg1-':M:T) cid + """ + ) + inside(res) { case Right((_, ValueContractId(cid), verificationRequests)) => + verificationRequests shouldBe List( + UpgradeVerificationRequest(cid, Set(alice), Set(bob), Some(v1_key)) + ) + } + } + + "be able to fetch by key a locally created contract using different versions" in { + val res = go( + e"""let alice : Party = '-pkg1-':M:mkParty "alice" + in ubind + cid: ContractId '-pkg1-':M:T <- '-pkg1-':M:do_create "alice" "bob" 100; + _: '-pkg2-':M:T <- fetch_by_key @'-pkg2-':M:T alice + in upure @(ContractId '-pkg1-':M:T) cid + """ + ) + inside(res) { case Right((_, ValueContractId(cid), verificationRequests)) => + verificationRequests shouldBe List( + UpgradeVerificationRequest(cid, Set(alice), Set(bob), Some(v2_key)), + UpgradeVerificationRequest(cid, Set(alice), Set(bob), Some(v2_key)), + ) + } + } + + "be able to exercise a locally created contract using different versions" in { + val res = go( + e"""ubind + cid: ContractId '-pkg1-':M:T <- '-pkg1-':M:do_create "alice" "bob" 100; + _: Unit <- exercise @'-pkg2-':M:T NoOp cid () + in upure @(ContractId '-pkg1-':M:T) cid + """ + ) + inside(res) { case Right((_, ValueContractId(cid), verificationRequests)) => + verificationRequests shouldBe List( + UpgradeVerificationRequest(cid, Set(alice), Set(bob), Some(v1_key)) + ) + } + } + + "be able to exercise by key a locally created contract using different versions" in { + val res = go( + e"""let alice : Party = '-pkg1-':M:mkParty "alice" + in ubind + cid: ContractId '-pkg1-':M:T <- '-pkg1-':M:do_create "alice" "bob" 100; + _: Unit <- exercise_by_key @'-pkg2-':M:T NoOp alice () + in upure @(ContractId '-pkg1-':M:T) cid + """ + ) + inside(res) { case Right((_, ValueContractId(cid), verificationRequests)) => + verificationRequests shouldBe List( + UpgradeVerificationRequest(cid, Set(alice), Set(bob), Some(v2_key)), + UpgradeVerificationRequest(cid, Set(alice), Set(bob), Some(v2_key)), + ) + } + } + + // TODO(https://github.com/digital-asset/daml/issues/20099): re-enable this test once fixed + "be able to exercise by interface locally created contract using different versions" ignore { + val res = go( + e"""let alice : Party = '-pkg1-':M:mkParty "alice" + in ubind + cid: ContractId '-pkg1-':M:T <- '-pkg1-':M:do_create "alice" "bob" 100; + res: Text <- exercise_interface @'-iface-':M:Iface + MyChoice + (COERCE_CONTRACT_ID @'-pkg1-':M:T @'-iface-':M:Iface cid) + alice + in upure @(ContractId '-pkg1-':M:T) cid + """, + packageResolution = Map(Ref.PackageName.assertFromString("-upgrade-test-") -> pkgId2), + ) + inside(res) { case Right((_, ValueContractId(cid), verificationRequests)) => + verificationRequests shouldBe List( + UpgradeVerificationRequest(cid, Set(alice), Set(bob), Some(v1_key)) + ) + } + } + "do recompute and check immutability of meta data when using different versions" in { // The following code is not properly typed, but emulates two commands that fetch a same contract using different versions. val res: Either[SError, (SValue, Value, List[UpgradeVerificationRequest])] = go(