diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapPackageConfigIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapPackageConfigIntegrationTest.scala index 756212dbfb..55beffbb48 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapPackageConfigIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/BootstrapPackageConfigIntegrationTest.scala @@ -168,13 +168,18 @@ class BootstrapPackageConfigIntegrationTest // This simulates an app vetting newer versions of their own DARs depending on newer splice-amulet versions // before the SVs do so. Topology aware package selection will then force the old splice-amulet and old splitwell versions // for composed transactions. Note that for this to work splitwell contracts must be downgradeable. + + // Split into batches to avoid gRPC message size limit (10 MB) + val batchSize = 12 + val versionBatches = + DarResources.splitwell.all.map(_.metadata.version).distinct.grouped(batchSize).toSeq + Seq(aliceValidatorBackend, bobValidatorBackend, splitwellValidatorBackend).foreach { p => - p.participantClient.dars.upload_many( - DarResources.splitwell.all - .map(_.metadata.version) - .distinct - .map((v: PackageVersion) => s"daml/dars/splitwell-$v.dar") - ) + versionBatches.foreach { batch => + p.participantClient.dars.upload_many( + batch.map((v: PackageVersion) => s"daml/dars/splitwell-$v.dar") + ) + } } } diff --git a/apps/common/frontend-test-handlers/src/mocks/handlers/validator-licenses-handler.ts b/apps/common/frontend-test-handlers/src/mocks/handlers/validator-licenses-handler.ts index 37375fd1c3..5bbb762cdc 100644 --- a/apps/common/frontend-test-handlers/src/mocks/handlers/validator-licenses-handler.ts +++ b/apps/common/frontend-test-handlers/src/mocks/handlers/validator-licenses-handler.ts @@ -21,6 +21,8 @@ export function validatorLicensesHandler(baseUrl: string): RestHandler { }, metadata: { version: '1', lastUpdatedAt: aTimestamp, contactPoint: 'nowhere' }, lastActiveAt: aTimestamp, + weight: null, + kind: null, }; return { contract_id: id, diff --git a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala index 1c1fc789c3..efb550de2c 100644 --- a/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala +++ b/apps/common/src/main/scala/org/lfdecentralizedtrust/splice/environment/DarResources.scala @@ -75,6 +75,7 @@ object DarResources { val amulet_0_1_12 = DarResource("splice-amulet-0.1.12.dar") val amulet_0_1_13 = DarResource("splice-amulet-0.1.13.dar") val amulet_0_1_14 = DarResource("splice-amulet-0.1.14.dar") + val amulet_0_1_15 = DarResource("splice-amulet-0.1.15.dar") val amulet_current = DarResource("splice-amulet-current.dar") val amulet = PackageResource( amulet_current, @@ -95,6 +96,7 @@ object DarResources { amulet_0_1_12, amulet_0_1_13, amulet_0_1_14, + amulet_0_1_15, ), ) @@ -119,6 +121,7 @@ object DarResources { val dsoGovernance_0_1_18 = DarResource("splice-dso-governance-0.1.18.dar") val dsoGovernance_0_1_19 = DarResource("splice-dso-governance-0.1.19.dar") val dsoGovernance_0_1_20 = DarResource("splice-dso-governance-0.1.20.dar") + val dsoGovernance_0_1_21 = DarResource("splice-dso-governance-0.1.21.dar") val dsoGovernance_current = DarResource("splice-dso-governance-current.dar") val dsoGovernance = PackageResource( dsoGovernance_current, @@ -145,6 +148,7 @@ object DarResources { dsoGovernance_0_1_18, dsoGovernance_0_1_19, dsoGovernance_0_1_20, + dsoGovernance_0_1_21, ), ) @@ -164,6 +168,7 @@ object DarResources { val amuletNameService_0_1_13 = DarResource("splice-amulet-name-service-0.1.13.dar") val amuletNameService_0_1_14 = DarResource("splice-amulet-name-service-0.1.14.dar") val amuletNameService_0_1_15 = DarResource("splice-amulet-name-service-0.1.15.dar") + val amuletNameService_0_1_16 = DarResource("splice-amulet-name-service-0.1.16.dar") val amuletNameService_current = DarResource("splice-amulet-name-service-current.dar") val amuletNameService = PackageResource( amuletNameService_current, @@ -185,6 +190,7 @@ object DarResources { amuletNameService_0_1_13, amuletNameService_0_1_14, amuletNameService_0_1_15, + amuletNameService_0_1_16, ), ) @@ -203,6 +209,7 @@ object DarResources { val splitwell_0_1_12 = DarResource("splitwell-0.1.12.dar") val splitwell_0_1_13 = DarResource("splitwell-0.1.13.dar") val splitwell_0_1_14 = DarResource("splitwell-0.1.14.dar") + val splitwell_0_1_15 = DarResource("splitwell-0.1.15.dar") val splitwell_current = DarResource("splitwell-current.dar") val splitwell = PackageResource( splitwell_current, @@ -223,6 +230,7 @@ object DarResources { splitwell_0_1_12, splitwell_0_1_13, splitwell_0_1_14, + splitwell_0_1_15, ), ) @@ -241,6 +249,7 @@ object DarResources { val wallet_0_1_12 = DarResource("splice-wallet-0.1.12.dar") val wallet_0_1_13 = DarResource("splice-wallet-0.1.13.dar") val wallet_0_1_14 = DarResource("splice-wallet-0.1.14.dar") + val wallet_0_1_15 = DarResource("splice-wallet-0.1.15.dar") val wallet_current = DarResource("splice-wallet-current.dar") val wallet = PackageResource( wallet_current, @@ -261,6 +270,7 @@ object DarResources { wallet_0_1_12, wallet_0_1_13, wallet_0_1_14, + wallet_0_1_15, ), ) @@ -279,6 +289,7 @@ object DarResources { val walletPayments_0_1_12 = DarResource("splice-wallet-payments-0.1.12.dar") val walletPayments_0_1_13 = DarResource("splice-wallet-payments-0.1.13.dar") val walletPayments_0_1_14 = DarResource("splice-wallet-payments-0.1.14.dar") + val walletPayments_0_1_15 = DarResource("splice-wallet-payments-0.1.15.dar") val walletPayments_current = DarResource("splice-wallet-payments-current.dar") val walletPayments = PackageResource( walletPayments_current, @@ -299,6 +310,7 @@ object DarResources { walletPayments_0_1_12, walletPayments_0_1_13, walletPayments_0_1_14, + walletPayments_0_1_15, ), ) diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/StoreTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/StoreTest.scala index c367add82a..d3f7deb8f0 100644 --- a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/StoreTest.scala +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/StoreTest.scala @@ -411,6 +411,8 @@ abstract class StoreTest extends AsyncWordSpec with BaseTest { ) ).toJava, Some(defaultEffectiveAt).toJava, + Optional.empty(), + Optional.empty(), ) contract( identifier = templateId, @@ -460,6 +462,7 @@ abstract class StoreTest extends AsyncWordSpec with BaseTest { dsoParty.toProtoPrimitive, validator.toProtoPrimitive, new Round(round), + Optional.empty(), ), ) } diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/ValueJsonCodecCodegenTest.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/ValueJsonCodecCodegenTest.scala index ae4f595c6e..73f185352b 100644 --- a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/ValueJsonCodecCodegenTest.scala +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/util/ValueJsonCodecCodegenTest.scala @@ -64,6 +64,8 @@ class ValueJsonCodecCodegenTest extends StoreTest with StoreErrors { Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), + Optional.empty(), ) // with (one) optional field set val decodedOptionalSet: JavaApi.DamlRecord = ValueJsonCodecCodegen @@ -81,6 +83,8 @@ class ValueJsonCodecCodegenTest extends StoreTest with StoreErrors { Optional.empty(), Optional.empty(), Optional.of(java.time.Instant.EPOCH), + Optional.empty(), + Optional.empty(), ) } diff --git a/apps/package-lock.json b/apps/package-lock.json index 35adf29730..369f3ebc32 100644 --- a/apps/package-lock.json +++ b/apps/package-lock.json @@ -27,13 +27,13 @@ "wallet/external-openapi-ts-client" ], "dependencies": { - "@daml.js/ans": "file:common/frontend/daml.js/splice-amulet-name-service-0.1.15", - "@daml.js/splice-amulet": "file:common/frontend/daml.js/splice-amulet-0.1.14", - "@daml.js/splice-dso-governance": "file:common/frontend/daml.js/splice-dso-governance-0.1.20", + "@daml.js/ans": "file:common/frontend/daml.js/splice-amulet-name-service-0.1.16", + "@daml.js/splice-amulet": "file:common/frontend/daml.js/splice-amulet-0.1.15", + "@daml.js/splice-dso-governance": "file:common/frontend/daml.js/splice-dso-governance-0.1.21", "@daml.js/splice-validator-lifecycle": "file:common/frontend/daml.js/splice-validator-lifecycle-0.1.5", - "@daml.js/splice-wallet": "file:common/frontend/daml.js/splice-wallet-0.1.14", - "@daml.js/splice-wallet-payments": "file:common/frontend/daml.js/splice-wallet-payments-0.1.14", - "@daml.js/splitwell": "file:common/frontend/daml.js/splitwell-0.1.14", + "@daml.js/splice-wallet": "file:common/frontend/daml.js/splice-wallet-0.1.15", + "@daml.js/splice-wallet-payments": "file:common/frontend/daml.js/splice-wallet-payments-0.1.15", + "@daml.js/splitwell": "file:common/frontend/daml.js/splitwell-0.1.15", "xunit-viewer": "^10.6.1" } }, @@ -320,6 +320,25 @@ "common/frontend/daml.js/splice-amulet-0.1.14": { "name": "@daml.js/splice-amulet-0.1.14", "version": "0.0.0", + "extraneous": true, + "license": "UNLICENSED", + "dependencies": { + "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", + "@daml.js/daml-stdlib-DA-Set-Types-1.0.0": "file:../daml-stdlib-DA-Set-Types-1.0.0", + "@daml.js/daml-stdlib-DA-Time-Types-1.0.0": "file:../daml-stdlib-DA-Time-Types-1.0.0", + "@daml.js/ghc-stdlib-DA-Internal-Template-1.0.0": "file:../ghc-stdlib-DA-Internal-Template-1.0.0", + "@daml.js/splice-api-featured-app-v1-1.0.0": "file:../splice-api-featured-app-v1-1.0.0", + "@daml.js/splice-api-token-allocation-instruction-v1-1.0.0": "file:../splice-api-token-allocation-instruction-v1-1.0.0", + "@daml.js/splice-api-token-allocation-v1-1.0.0": "file:../splice-api-token-allocation-v1-1.0.0", + "@daml.js/splice-api-token-holding-v1-1.0.0": "file:../splice-api-token-holding-v1-1.0.0", + "@daml.js/splice-api-token-metadata-v1-1.0.0": "file:../splice-api-token-metadata-v1-1.0.0", + "@daml.js/splice-api-token-transfer-instruction-v1-1.0.0": "file:../splice-api-token-transfer-instruction-v1-1.0.0", + "@mojotech/json-type-validation": "^3.1.0" + } + }, + "common/frontend/daml.js/splice-amulet-0.1.15": { + "name": "@daml.js/splice-amulet-0.1.15", + "version": "0.0.0", "license": "UNLICENSED", "dependencies": { "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", @@ -422,6 +441,7 @@ "common/frontend/daml.js/splice-amulet-name-service-0.1.15": { "name": "@daml.js/splice-amulet-name-service-0.1.15", "version": "0.0.0", + "extraneous": true, "license": "UNLICENSED", "dependencies": { "@daml.js/daml-stdlib-DA-Time-Types-1.0.0": "file:../daml-stdlib-DA-Time-Types-1.0.0", @@ -432,6 +452,19 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splice-amulet-name-service-0.1.16": { + "name": "@daml.js/splice-amulet-name-service-0.1.16", + "version": "0.0.0", + "license": "UNLICENSED", + "dependencies": { + "@daml.js/daml-stdlib-DA-Time-Types-1.0.0": "file:../daml-stdlib-DA-Time-Types-1.0.0", + "@daml.js/ghc-stdlib-DA-Internal-Template-1.0.0": "file:../ghc-stdlib-DA-Internal-Template-1.0.0", + "@daml.js/splice-amulet-0.1.15": "file:../splice-amulet-0.1.15", + "@daml.js/splice-api-featured-app-v1-1.0.0": "file:../splice-api-featured-app-v1-1.0.0", + "@daml.js/splice-wallet-payments-0.1.15": "file:../splice-wallet-payments-0.1.15", + "@mojotech/json-type-validation": "^3.1.0" + } + }, "common/frontend/daml.js/splice-amulet-name-service-0.1.9": { "name": "@daml.js/splice-amulet-name-service-0.1.9", "version": "0.0.0", @@ -555,6 +588,7 @@ "common/frontend/daml.js/splice-dso-governance-0.1.20": { "name": "@daml.js/splice-dso-governance-0.1.20", "version": "0.0.0", + "extraneous": true, "license": "UNLICENSED", "dependencies": { "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", @@ -567,6 +601,21 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splice-dso-governance-0.1.21": { + "name": "@daml.js/splice-dso-governance-0.1.21", + "version": "0.0.0", + "license": "UNLICENSED", + "dependencies": { + "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", + "@daml.js/daml-stdlib-DA-Set-Types-1.0.0": "file:../daml-stdlib-DA-Set-Types-1.0.0", + "@daml.js/daml-stdlib-DA-Time-Types-1.0.0": "file:../daml-stdlib-DA-Time-Types-1.0.0", + "@daml.js/ghc-stdlib-DA-Internal-Template-1.0.0": "file:../ghc-stdlib-DA-Internal-Template-1.0.0", + "@daml.js/splice-amulet-0.1.15": "file:../splice-amulet-0.1.15", + "@daml.js/splice-amulet-name-service-0.1.16": "file:../splice-amulet-name-service-0.1.16", + "@daml.js/splice-wallet-payments-0.1.15": "file:../splice-wallet-payments-0.1.15", + "@mojotech/json-type-validation": "^3.1.0" + } + }, "common/frontend/daml.js/splice-validator-lifecycle-0.1.3": { "name": "@daml.js/splice-validator-lifecycle-0.1.3", "version": "0.0.0", @@ -667,6 +716,7 @@ "common/frontend/daml.js/splice-wallet-0.1.14": { "name": "@daml.js/splice-wallet-0.1.14", "version": "0.0.0", + "extraneous": true, "license": "UNLICENSED", "dependencies": { "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", @@ -680,6 +730,22 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splice-wallet-0.1.15": { + "name": "@daml.js/splice-wallet-0.1.15", + "version": "0.0.0", + "license": "UNLICENSED", + "dependencies": { + "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", + "@daml.js/daml-stdlib-DA-Time-Types-1.0.0": "file:../daml-stdlib-DA-Time-Types-1.0.0", + "@daml.js/ghc-stdlib-DA-Internal-Template-1.0.0": "file:../ghc-stdlib-DA-Internal-Template-1.0.0", + "@daml.js/splice-amulet-0.1.15": "file:../splice-amulet-0.1.15", + "@daml.js/splice-api-token-allocation-instruction-v1-1.0.0": "file:../splice-api-token-allocation-instruction-v1-1.0.0", + "@daml.js/splice-api-token-allocation-v1-1.0.0": "file:../splice-api-token-allocation-v1-1.0.0", + "@daml.js/splice-api-token-transfer-instruction-v1-1.0.0": "file:../splice-api-token-transfer-instruction-v1-1.0.0", + "@daml.js/splice-wallet-payments-0.1.15": "file:../splice-wallet-payments-0.1.15", + "@mojotech/json-type-validation": "^3.1.0" + } + }, "common/frontend/daml.js/splice-wallet-0.1.9": { "name": "@daml.js/splice-wallet-0.1.9", "version": "0.0.0", @@ -752,6 +818,7 @@ "common/frontend/daml.js/splice-wallet-payments-0.1.14": { "name": "@daml.js/splice-wallet-payments-0.1.14", "version": "0.0.0", + "extraneous": true, "license": "UNLICENSED", "dependencies": { "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", @@ -761,6 +828,18 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splice-wallet-payments-0.1.15": { + "name": "@daml.js/splice-wallet-payments-0.1.15", + "version": "0.0.0", + "license": "UNLICENSED", + "dependencies": { + "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", + "@daml.js/daml-stdlib-DA-Time-Types-1.0.0": "file:../daml-stdlib-DA-Time-Types-1.0.0", + "@daml.js/ghc-stdlib-DA-Internal-Template-1.0.0": "file:../ghc-stdlib-DA-Internal-Template-1.0.0", + "@daml.js/splice-amulet-0.1.15": "file:../splice-amulet-0.1.15", + "@mojotech/json-type-validation": "^3.1.0" + } + }, "common/frontend/daml.js/splice-wallet-payments-0.1.9": { "name": "@daml.js/splice-wallet-payments-0.1.9", "version": "0.0.0", @@ -833,6 +912,7 @@ "common/frontend/daml.js/splitwell-0.1.14": { "name": "@daml.js/splitwell-0.1.14", "version": "0.0.0", + "extraneous": true, "license": "UNLICENSED", "dependencies": { "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", @@ -843,6 +923,19 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splitwell-0.1.15": { + "name": "@daml.js/splitwell-0.1.15", + "version": "0.0.0", + "license": "UNLICENSED", + "dependencies": { + "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", + "@daml.js/daml-stdlib-DA-Time-Types-1.0.0": "file:../daml-stdlib-DA-Time-Types-1.0.0", + "@daml.js/ghc-stdlib-DA-Internal-Template-1.0.0": "file:../ghc-stdlib-DA-Internal-Template-1.0.0", + "@daml.js/splice-amulet-0.1.15": "file:../splice-amulet-0.1.15", + "@daml.js/splice-wallet-payments-0.1.15": "file:../splice-wallet-payments-0.1.15", + "@mojotech/json-type-validation": "^3.1.0" + } + }, "common/frontend/daml.js/splitwell-0.1.9": { "name": "@daml.js/splitwell-0.1.9", "version": "0.0.0", @@ -1312,7 +1405,7 @@ } }, "node_modules/@daml.js/ans": { - "resolved": "common/frontend/daml.js/splice-amulet-name-service-0.1.15", + "resolved": "common/frontend/daml.js/splice-amulet-name-service-0.1.16", "link": true }, "node_modules/@daml.js/daml-prim-DA-Types-1.0.0": { @@ -1332,15 +1425,15 @@ "link": true }, "node_modules/@daml.js/splice-amulet": { - "resolved": "common/frontend/daml.js/splice-amulet-0.1.14", + "resolved": "common/frontend/daml.js/splice-amulet-0.1.15", "link": true }, - "node_modules/@daml.js/splice-amulet-0.1.14": { - "resolved": "common/frontend/daml.js/splice-amulet-0.1.14", + "node_modules/@daml.js/splice-amulet-0.1.15": { + "resolved": "common/frontend/daml.js/splice-amulet-0.1.15", "link": true }, - "node_modules/@daml.js/splice-amulet-name-service-0.1.15": { - "resolved": "common/frontend/daml.js/splice-amulet-name-service-0.1.15", + "node_modules/@daml.js/splice-amulet-name-service-0.1.16": { + "resolved": "common/frontend/daml.js/splice-amulet-name-service-0.1.16", "link": true }, "node_modules/@daml.js/splice-api-featured-app-v1-1.0.0": { @@ -1380,7 +1473,7 @@ "link": true }, "node_modules/@daml.js/splice-dso-governance": { - "resolved": "common/frontend/daml.js/splice-dso-governance-0.1.20", + "resolved": "common/frontend/daml.js/splice-dso-governance-0.1.21", "link": true }, "node_modules/@daml.js/splice-validator-lifecycle": { @@ -1388,19 +1481,19 @@ "link": true }, "node_modules/@daml.js/splice-wallet": { - "resolved": "common/frontend/daml.js/splice-wallet-0.1.14", + "resolved": "common/frontend/daml.js/splice-wallet-0.1.15", "link": true }, "node_modules/@daml.js/splice-wallet-payments": { - "resolved": "common/frontend/daml.js/splice-wallet-payments-0.1.14", + "resolved": "common/frontend/daml.js/splice-wallet-payments-0.1.15", "link": true }, - "node_modules/@daml.js/splice-wallet-payments-0.1.14": { - "resolved": "common/frontend/daml.js/splice-wallet-payments-0.1.14", + "node_modules/@daml.js/splice-wallet-payments-0.1.15": { + "resolved": "common/frontend/daml.js/splice-wallet-payments-0.1.15", "link": true }, "node_modules/@daml.js/splitwell": { - "resolved": "common/frontend/daml.js/splitwell-0.1.14", + "resolved": "common/frontend/daml.js/splitwell-0.1.15", "link": true }, "node_modules/@daml/ledger": { diff --git a/apps/package.json b/apps/package.json index 238de993a0..f5ed4649c9 100644 --- a/apps/package.json +++ b/apps/package.json @@ -22,13 +22,13 @@ "wallet/external-openapi-ts-client" ], "dependencies": { - "@daml.js/ans": "file:common/frontend/daml.js/splice-amulet-name-service-0.1.15", - "@daml.js/splice-amulet": "file:common/frontend/daml.js/splice-amulet-0.1.14", - "@daml.js/splice-dso-governance": "file:common/frontend/daml.js/splice-dso-governance-0.1.20", + "@daml.js/ans": "file:common/frontend/daml.js/splice-amulet-name-service-0.1.16", + "@daml.js/splice-amulet": "file:common/frontend/daml.js/splice-amulet-0.1.15", + "@daml.js/splice-dso-governance": "file:common/frontend/daml.js/splice-dso-governance-0.1.21", "@daml.js/splice-validator-lifecycle": "file:common/frontend/daml.js/splice-validator-lifecycle-0.1.5", - "@daml.js/splice-wallet": "file:common/frontend/daml.js/splice-wallet-0.1.14", - "@daml.js/splice-wallet-payments": "file:common/frontend/daml.js/splice-wallet-payments-0.1.14", - "@daml.js/splitwell": "file:common/frontend/daml.js/splitwell-0.1.14", + "@daml.js/splice-wallet": "file:common/frontend/daml.js/splice-wallet-0.1.15", + "@daml.js/splice-wallet-payments": "file:common/frontend/daml.js/splice-wallet-payments-0.1.15", + "@daml.js/splitwell": "file:common/frontend/daml.js/splitwell-0.1.15", "xunit-viewer": "^10.6.1" } } diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/confirmation/SummarizingMiningRoundTrigger.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/confirmation/SummarizingMiningRoundTrigger.scala index 59eb535962..ce81ffcfe8 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/confirmation/SummarizingMiningRoundTrigger.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/automation/confirmation/SummarizingMiningRoundTrigger.scala @@ -201,7 +201,10 @@ object SummarizingMiningRoundTrigger { featuredAppRewardCoupons.bigDecimal, unfeaturedAppRewardCoupons.bigDecimal, svRewardCouponsWeightSum, - Optional.of(validatorLivenessActivityRecords), + Optional.empty(), // optTotalValidatorFaucetCoupons (deprecated) + Optional.of( + java.math.BigDecimal.valueOf(validatorLivenessActivityRecords) + ), // optTotalValidatorLivenessActivityRecords ) override def pretty: Pretty[this.type] = diff --git a/daml/dars.lock b/daml/dars.lock index 648dad59a6..2a2b38172a 100644 --- a/daml/dars.lock +++ b/daml/dars.lock @@ -5,6 +5,7 @@ splice-amulet 0.1.11 9824927cdb455f833867b74c01cffcd8cb8cc5edd4d2273cea1329b708e splice-amulet 0.1.12 95a88ff9ffd509e097802ecf3bbd58c83a5dff408e439cca4e2105ebd2cd0760 splice-amulet 0.1.13 6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0c990c splice-amulet 0.1.14 3ca1343ab26b453d38c8adb70dca5f1ead8440c42b59b68f070786955cbf9ec1 +splice-amulet 0.1.15 0ee7b4ce705113f9748a3d36f199bcdf508c28985644dbe24b58cb65e85d68cd splice-amulet 0.1.2 1446ffdf23326cef2de97923df96618eb615792bea36cf1431f03639448f1645 splice-amulet 0.1.3 0d89016d5a90eb8bced48bbac99e81c57781b3a36094b8d48b8e4389851e19af splice-amulet 0.1.4 a36ef8888fb44caae13d96341ce1fabd84fc9e2e7b209bbc3caabb48b6be1668 @@ -21,6 +22,7 @@ splice-amulet-name-service 0.1.12 557a74491324790b5cf5a379f2481ab1cd2c8b75530858 splice-amulet-name-service 0.1.13 0e8c7e1cb828336bb4d537f6c4ecd94128b94733eb2ae1f55b5962757d357b4b splice-amulet-name-service 0.1.14 6cb1318176e758c256c2e385f87b86c5060e80fb68a72e8ceb08ac5f9045fff2 splice-amulet-name-service 0.1.15 d4724b90dce9fb08badbb367962d237710b3a603e4f57806a1b0af308cc70fdb +splice-amulet-name-service 0.1.16 e65a5b3dbf85a4f519de28929196560956d7c94e3731e2218967c0aea352bbd1 splice-amulet-name-service 0.1.2 711a2974d65e6ebd149704da75f3f71234798687ab895b92f066c865dbdeeabb splice-amulet-name-service 0.1.3 beb4b85f3f0cf36dfb93fc917d3ac218ee5d41b6e70604720cb228d85e168ee0 splice-amulet-name-service 0.1.4 053c7f4c2a77312e7d465a4fa7dc8cb298754ad12c0c987a7c401bd724e65efc @@ -29,8 +31,8 @@ splice-amulet-name-service 0.1.6 a208aab2c4a248ab2eff352bd382f8b3bbadc92464123db splice-amulet-name-service 0.1.7 ba7806d9b2d593eac74a050161c54ae1325d170bf175cb66a9c1e5e5ffb88c3d splice-amulet-name-service 0.1.8 efeb3f9b2b92e55fac4ec2d6164f95407a01477240c7465e576df4e310f54bd3 splice-amulet-name-service 0.1.9 f1b5915ad45ded616f43f83c735b7ee158b5eb58abe758a721e50eee19b3e531 -splice-amulet-name-service-test 0.1.17 ebbf4bf6ad17264136d41eedbd1b9c4b4f67dfd5da102edec5792f2d38329b00 -splice-amulet-test 0.1.16 9281ddba976c6fb91929a3f58fac8d7301028cfd38fb3d645781fe168f101dc5 +splice-amulet-name-service-test 0.1.18 ca7f53160acd4328a6ef9b98c7601edcf9af76813af4c1cd87dc6e644f811f72 +splice-amulet-test 0.1.17 26de365e3a2c68aea02f581fe1437267bce9fc88427155ba482a823ea7d08a5c splice-api-featured-app-v1 1.0.0 7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda splice-api-token-allocation-instruction-v1 1.0.0 275064aacfe99cea72ee0c80563936129563776f67415ef9f13e4297eecbc520 splice-api-token-allocation-request-v1 1.0.0 6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193 @@ -55,6 +57,7 @@ splice-dso-governance 0.1.18 136484875714fcf24c24858e1d573ac756524eeb607c26999fc splice-dso-governance 0.1.19 759d1cf002fc1225ac43a55d73f0058becce3e62cfb5485c197f2b69ed8d9d98 splice-dso-governance 0.1.2 4206e127be8b111ac84bd7f98bd9dbf03ed489f1642b46ab31a46ee6d688e7e8 splice-dso-governance 0.1.20 996a3b619d6b65ca7812881978c44c650cac119de78f5317d1f317658943001c +splice-dso-governance 0.1.21 e03bdd0e09404e3cf4b0a86967d38d24fe770fee96634a139416c937bc85fd2c splice-dso-governance 0.1.3 b0ae3cc03e418790305a3c15f761fe495572de5827f8d322fb8b96996b783c13 splice-dso-governance 0.1.4 dc24fd18b4d151cd1e0ff6bfb7438bafb2f50fe076d0f16f50565e60b153a0be splice-dso-governance 0.1.5 9e3ca1d22ad495dfabf3d61acae3dc1a7718f527f02092280b58cf69edfdc84c @@ -62,8 +65,8 @@ splice-dso-governance 0.1.6 4e7653cfbf7ca249de4507aca9cd3b91060e5489042a522c589d splice-dso-governance 0.1.7 d406eba1132d464605f4dae3edf8cf5ecbbb34bd8edef0e047e7e526d328718c splice-dso-governance 0.1.8 1790a114f83d5f290261fae1e7e46fba75a861a3dd603c6b4ef6b67b49053948 splice-dso-governance 0.1.9 9ee83bfd872f91e659b8a8439c5b4eaf240bcf6f19698f884d7d7993ab48c401 -splice-dso-governance-test 0.1.23 2a716b9302ff9ae53b807c1815949011bae4f9f3f7c8e98e364b36a78a26a174 -splice-token-standard-test 1.0.7 89c1deed6db53114ba712712ec1215d536d2544e01af271adc85fe2d5f4dd5b6 +splice-dso-governance-test 0.1.24 9fc27d76e86db28d2495e0a2b0f82848b4548c6676ca546491522abc1c81913a +splice-token-standard-test 1.0.8 c737ca98e9415611f46d8a5f965cc2f113c487866a6aebc5fe78f4aaaa4aa1ff splice-token-test-dummy-holding 0.0.1 1cd171c6c42ab46dc9cf12d80c6111369e00cea5cdf054924b4f26ce94b1ef5b splice-token-test-dummy-holding 0.0.2 4f40fb033ef3db89623642c1b494e846097fa32af138b3864a63aa15937a323d splice-token-test-trading-app 1.0.0 e5c9847d5a88d3b8d65436f01765fc5ba142cc58529692e2dacdd865d9939f71 @@ -75,9 +78,9 @@ splice-util 0.1.4 b7356fbb2cf8a3b22194d8c743c3c216d9c7527b257c8c38b257eb22942be3 splice-util-featured-app-proxies 1.0.0 48e0c4fe4ea05e3b740404ebe37004ddd741efbdcd665c1c3199a5d6d9d944d7 splice-util-featured-app-proxies 1.1.0 81dd5a9e5c02d0de03208522a895fb85eeb12fbea4aca7c4ad0ad106f3b0bfce splice-util-featured-app-proxies 1.2.0 653c48879064332d34af5008bdfd8e349493460e67e62b85e8e7e3392831c842 -splice-util-featured-app-proxies-test 1.0.4 ef633c6eff2d786cc571e58f572c5a0c72d31b7ffc1eeeedc420791c50b9a7f1 +splice-util-featured-app-proxies-test 1.0.5 b98cd185a09bb29dd5dfbfb4de00a4fba664c0fb0051c3adbcf844de885daefe splice-util-token-standard-wallet 1.0.0 1da198cb7968fa478cfa12aba9fdf128a63a8af6ab284ea6be238cf92a3733ac -splice-util-token-standard-wallet-test 1.0.0 ee328d4744bce339f040078d7271a3d73c932d23589cfa1cfbd99aaf6408c3c2 +splice-util-token-standard-wallet-test 1.0.1 6b9dfbe788401d0a3cda7a56d40d6a50a2d02c3c2ae2ddfc6ab91d2742c4f8c8 splice-validator-lifecycle 0.1.0 cef96fac957362f1fc097120bd13686cac7f84fbc8053afa994a1f9214d9570c splice-validator-lifecycle 0.1.1 1ddf05c96002914593c929848b786f34c753fb0be07717d1786be177a564aada splice-validator-lifecycle 0.1.2 57e2f15f9755db1f00e51c52c319294264a21ad71c6bc1e7cd70db4b164c0aaa @@ -92,6 +95,7 @@ splice-wallet 0.1.11 991842eee48ec3caa3a649e8f47e3544dd7b688ce4b363aa934a83db7da splice-wallet 0.1.12 b30bb727552cf6b624dbc9a5ff95f6c158e0a654e2e9c5c27bcfe3f5d0f9ada2 splice-wallet 0.1.13 eb6e01efacc3397e23c6be8b9be7db4bf37672211974d69e24b48980e2f98b7e splice-wallet 0.1.14 690c1d47bac06db419db344d59a7a30c53fa3f5d961943fe1782cfc6c78794d8 +splice-wallet 0.1.15 941bd097a2ff4166b3b3100c211f3c890f994d97406fd206cfab0cd7fbf7ce1f splice-wallet 0.1.2 c162e08a4ec0428bfa870b6d9040989e575c74199c3a80558c62e03196dd5146 splice-wallet 0.1.3 2c35bb4f5084ea66db59717d21750bfd64c43147ef5fd5166615092d592a6917 splice-wallet 0.1.4 141dad2d33b6410b8e1c35a0c4f8f76cb691e4d9a4410ce89f33f373855317e1 @@ -107,6 +111,7 @@ splice-wallet-payments 0.1.11 7266d861727757f3482857a77f25f4d647d8925b469e46938a splice-wallet-payments 0.1.12 88516902a9f045d3fd3835c8f5c8c6bfe4b44d83fae11369241f1883bb5b3ab4 splice-wallet-payments 0.1.13 0b9250642d3864e6bbea553264dcac0d286104f24efad2fbaf4645520bcb4053 splice-wallet-payments 0.1.14 45b29d6e05b5352c39edde850c66b4535c682b9991b06eec312176b1a48ecab5 +splice-wallet-payments 0.1.15 6be0d759d1a665b8ae2348f22a7c7da0cd09ba7d8d5e1796eda58e8e73bf91d5 splice-wallet-payments 0.1.2 775f5eb9c0249509adda5eb3ea4ee31bb953601168c18880df6f2ff09ec4298a splice-wallet-payments 0.1.3 b953b3729c81a55e598a364be7d0c0574750df3de12a7a1b53a300f217cb5c5c splice-wallet-payments 0.1.4 12177f54873c1094ea169874ad0d7838383fd137f302d16356e93f28dfbc0fcc @@ -115,7 +120,7 @@ splice-wallet-payments 0.1.6 6124379528eeb6fa17ecdab15577c29abb33d0c0d34dc5f2680 splice-wallet-payments 0.1.7 4e3e0d9cdadf80f4bf8f3cd3660d5287c084c9a29f23c901aabce597d72fd467 splice-wallet-payments 0.1.8 e48ea337ee3335c8bb3206a2501ce947ac1a7bdb1825cee8f28bad64f5a7bc4b splice-wallet-payments 0.1.9 7f4e081ad96f2ccded0c053b0cf5ddddae1139dfc3bb89cefcf77ea70f2cecb7 -splice-wallet-test 0.1.16 ca90edb43d39f417847d0d9e9eb03dc9e1aa4592efa4d992713cb4d173caddeb +splice-wallet-test 0.1.17 7e0d78fd70aa414aa88921843da913c800a3e36a24d85c6ce7d40e51dfc91cf3 splitwell 0.1.0 075c76de553ab88383a7c69de134afa82aacfdf8ea8fcfe8852c4b199c3b2669 splitwell 0.1.1 ccb1a0215053062202052e1a052f9214da3fdae5253a6d43e2e155ff4f57fe75 splitwell 0.1.10 d42676a366f7ca7a2409974dd3054aa4d83ab29baa3b2086ad021407b0a1a295 @@ -123,6 +128,7 @@ splitwell 0.1.11 03b487fa26a8ef67df0876fb337904624c3fac27f11b7ad2d131a4eab26ee1b splitwell 0.1.12 cc047977ee8da70e858f203a14c3fd302c6aaed27be42383e61a026854d76112 splitwell 0.1.13 c2cf7b5fb3c615cdd2c8e14af42f1ca5fe4df8647cb656c7d02a72420152c3dd splitwell 0.1.14 bf2ec3fec9bcb58ed5e2ff63072a1e4994d0415ea7a0275942be282906a42021 +splitwell 0.1.15 2d1f9538d3bc77f28ea9d72c4d17cc3901a2623a236f1f41e7dd3a0d9dc94d7b splitwell 0.1.2 778edd2c228c6b68198d4d033885b2d0dae7daaee55d7df3edd9dfdf1f10fbd0 splitwell 0.1.3 7cde068cde689584f86a2499689d5cb165264d96496721e24ac6fb909f770a58 splitwell 0.1.4 85557b86cd4f330f093915db1ea26eac5092de6b5ddae0690146f6059c89419b @@ -131,4 +137,4 @@ splitwell 0.1.6 872da0dd7986fd768930f85d6a7310a94a0ef924e7fbb7bb7a4e149f2b5feb74 splitwell 0.1.7 841d1c9c86b5c8f3a39059459ecd8febedf7703e18f117300bb0ebf4423db096 splitwell 0.1.8 63b8153a08ceb4bf40d807acc5712372c3eac548c266be4d5e92470b4f655515 splitwell 0.1.9 b6267905698d2798b9ef171e27d49fb88e052ec0ec0e0675a3a1b275c7d037d4 -splitwell-test 0.1.16 3b6da31da597297daf5204019599766ce876ae255b98bbbfa59e68fd83116170 \ No newline at end of file +splitwell-test 0.1.17 ea948d1b956a5f065af06d1534c7d5a0708be749c6d251701874cc2eaff2c326 \ No newline at end of file diff --git a/daml/dars/splice-amulet-0.1.15.dar b/daml/dars/splice-amulet-0.1.15.dar new file mode 100644 index 0000000000..b6679a535a Binary files /dev/null and b/daml/dars/splice-amulet-0.1.15.dar differ diff --git a/daml/dars/splice-amulet-name-service-0.1.16.dar b/daml/dars/splice-amulet-name-service-0.1.16.dar new file mode 100644 index 0000000000..a9d36f2138 Binary files /dev/null and b/daml/dars/splice-amulet-name-service-0.1.16.dar differ diff --git a/daml/dars/splice-dso-governance-0.1.21.dar b/daml/dars/splice-dso-governance-0.1.21.dar new file mode 100644 index 0000000000..4d1d62ee04 Binary files /dev/null and b/daml/dars/splice-dso-governance-0.1.21.dar differ diff --git a/daml/dars/splice-wallet-0.1.15.dar b/daml/dars/splice-wallet-0.1.15.dar new file mode 100644 index 0000000000..ac369b02b3 Binary files /dev/null and b/daml/dars/splice-wallet-0.1.15.dar differ diff --git a/daml/dars/splice-wallet-payments-0.1.15.dar b/daml/dars/splice-wallet-payments-0.1.15.dar new file mode 100644 index 0000000000..3e341b3a96 Binary files /dev/null and b/daml/dars/splice-wallet-payments-0.1.15.dar differ diff --git a/daml/dars/splitwell-0.1.15.dar b/daml/dars/splitwell-0.1.15.dar new file mode 100644 index 0000000000..df215d0951 Binary files /dev/null and b/daml/dars/splitwell-0.1.15.dar differ diff --git a/daml/splice-amulet-name-service-test/daml.yaml b/daml/splice-amulet-name-service-test/daml.yaml index 491afa4ead..6f34b4eade 100644 --- a/daml/splice-amulet-name-service-test/daml.yaml +++ b/daml/splice-amulet-name-service-test/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 name: splice-amulet-name-service-test source: daml -version: 0.1.17 +version: 0.1.18 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-amulet-name-service/daml.yaml b/daml/splice-amulet-name-service/daml.yaml index 83452c24b4..7259ecb172 100644 --- a/daml/splice-amulet-name-service/daml.yaml +++ b/daml/splice-amulet-name-service/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 name: splice-amulet-name-service source: daml -version: 0.1.15 +version: 0.1.16 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-amulet-test/daml.yaml b/daml/splice-amulet-test/daml.yaml index 058d42484a..af4110d5ac 100644 --- a/daml/splice-amulet-test/daml.yaml +++ b/daml/splice-amulet-test/daml.yaml @@ -6,7 +6,7 @@ sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 name: splice-amulet-test source: daml -version: 0.1.16 +version: 0.1.17 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/TestUnclaimedRewards.daml b/daml/splice-amulet-test/daml/Splice/Scripts/TestUnclaimedRewards.daml deleted file mode 100644 index de639b25d4..0000000000 --- a/daml/splice-amulet-test/daml/Splice/Scripts/TestUnclaimedRewards.daml +++ /dev/null @@ -1,193 +0,0 @@ --- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. --- SPDX-License-Identifier: Apache-2.0 - -module Splice.Scripts.TestUnclaimedRewards where - -import DA.Action (when, void) -import DA.Assert - -import Daml.Script - -import Splice.Amulet -import Splice.AmuletRules -import Splice.Issuance -import Splice.Round -import Splice.Types -import Splice.ValidatorLicense -import Splice.Scripts.Util -import Splice.Testing.Registries.AmuletRegistry.Parameters - -totalRewardsPerRound : Decimal -totalRewardsPerRound = issuanceConfig_0_0p5.amuletToIssuePerYear / (365.0 * 24.0 * 6.0) -- one round every 10' - -testUnclaimedRewards_noActivity : Script () -testUnclaimedRewards_noActivity = do - DefaultAppWithUsers{..} <- setupDefaultAppWithUsers - - -- start issuing - runNextIssuance app - runAmuletDepositBots app - - -- check size of unclaimed reward issuance - mergeAndCheckUnclaimedRewardAmount app totalRewardsPerRound - - -testUnclaimedRewards_withCoupons : Script () -testUnclaimedRewards_withCoupons = do - DefaultAppWithUsers{..} <- setupDefaultAppWithUsers - - -- bare-create all three kinds of rewards - appCoupon1 <- submitMulti [app.dso, provider1.primaryParty] [] $ createCmd AppRewardCoupon with - dso = app.dso - provider = provider1.primaryParty - beneficiary = None - amount = 5.7 - round = Round 0 - featured = True - - appCoupon2 <- submitMulti [app.dso, provider1.primaryParty] [] $ createCmd AppRewardCoupon with - dso = app.dso - provider = provider1.primaryParty - beneficiary = None - amount = 3.3 - round = Round 0 - featured = False - - validatorCoupon1 <- submitMulti [app.dso, alice.primaryParty] [] $ createCmd ValidatorRewardCoupon with - dso = app.dso - user = alice.primaryParty - amount = 2.3 - round = Round 0 - - validatorFaucet1 <- submitMulti [app.dso, alice.primaryParty] [] $ createCmd ValidatorFaucetCoupon with - dso = app.dso - validator = alice.primaryParty - round = Round 0 - - validatorFaucet2 <- submitMulti [app.dso, provider1Validator.primaryParty] [] $ createCmd ValidatorFaucetCoupon with - dso = app.dso - validator = provider1Validator.primaryParty - round = Round 0 - - validatorLivenessActivityRecord1 <- submitMulti [app.dso] [] $ createCmd ValidatorLivenessActivityRecord with - dso = app.dso - validator = alice.primaryParty - round = Round 0 - - validatorLivenessActivityRecord2 <- submitMulti [app.dso] [] $ createCmd ValidatorLivenessActivityRecord with - dso = app.dso - validator = provider1Validator.primaryParty - round = Round 0 - - svRewardCoupon1 <- submitMulti [app.dso, provider2.primaryParty] [] $ createCmd SvRewardCoupon with - dso = app.dso - sv = provider2.primaryParty - beneficiary = provider2.primaryParty - round = Round 0 - weight = 10 - - -- -- start issuing - runNextIssuance app - runAmuletDepositBots app - - [(_, issuing)] <- query @IssuingMiningRound app.dso - - -- check size of unclaimed reward issuance - let allocatedRewards = - 5.7 * issuanceConfig_0_0p5.featuredAppRewardCap + - 3.3 * issuanceConfig_0_0p5.unfeaturedAppRewardCap + - 2.3 * issuanceConfig_0_0p5.validatorRewardCap + - 2.0 * getValidatorFaucetCap issuanceConfig_0_0p5 + - 2.0 * getValidatorFaucetCap issuanceConfig_0_0p5 + - 10.0 * issuing.issuancePerSvRewardCoupon - mergeAndCheckUnclaimedRewardAmount app (totalRewardsPerRound - allocatedRewards) - - -- run another two issuances - runNextIssuance app - runNextIssuance app - - let numIssuingRoundsToDate = 3.0 - mergeAndCheckUnclaimedRewardAmount app (numIssuingRoundsToDate * totalRewardsPerRound - allocatedRewards) - - -- run another two issuance such that round 0 becomes closed - runNextIssuance app - - -- collect the reward coupons - [(closedRoundCid, _)] <- query @ClosedMiningRound app.dso - let collect = AmuletRules_ClaimExpiredRewards with - closedRoundCid - validatorRewardCouponCids = [validatorCoupon1] - appCouponCids = [appCoupon1, appCoupon2] - svRewardCouponCids = [svRewardCoupon1] - optValidatorFaucetCouponCids = Some [validatorFaucet1, validatorFaucet2] - optValidatorLivenessActivityRecordCids = Some [validatorLivenessActivityRecord1, validatorLivenessActivityRecord2] - submitExerciseAmuletRulesByKey app [app.dso] [] collect - - -- check that the addition is done correctly - let numIssuingRoundsToDate = 4.0 - -- note that now the reward coupons have been collected as well, and we thus - -- don't have to subtract the allocatedRewards anymore. They've been merged back - -- into the `UnclaimedReward`s fund. - mergeAndCheckUnclaimedRewardAmount app (numIssuingRoundsToDate * totalRewardsPerRound) - pure () - -testNoUnclaimedRewards : Script () -testNoUnclaimedRewards = do - DefaultAppWithUsers{..} <- setupDefaultAppWithUsers - - -- bare-create enough rewards that there are no unclaimed rewards - _ <- submitMulti [app.dso, provider1.primaryParty] [] $ createCmd AppRewardCoupon with - dso = app.dso - provider = provider1.primaryParty - beneficiary = None - amount = 4000.0 - round = Round 0 - featured = True - - _ <- submitMulti [app.dso, provider1.primaryParty] [] $ createCmd AppRewardCoupon with - dso = app.dso - provider = provider1.primaryParty - beneficiary = None - amount = 1000.0 - round = Round 0 - featured = False - - _ <- submitMulti [app.dso, alice.primaryParty] [] $ createCmd ValidatorRewardCoupon with - dso = app.dso - user = alice.primaryParty - amount = 30000000.0 - round = Round 0 - - _ <- submitMulti [app.dso, provider2.primaryParty] [] $ createCmd SvRewardCoupon with - dso = app.dso - sv = provider2.primaryParty - beneficiary = provider2.primaryParty - round = Round 0 - weight = 15 - - -- -- start issuing - runNextIssuance app - runAmuletDepositBots app - - unclaimedRewards <- query @UnclaimedReward app.dso - unclaimedRewards === [] - pure () - -mergeAndCheckUnclaimedRewardAmount : AmuletApp -> Decimal -> Script () -mergeAndCheckUnclaimedRewardAmount app expectedAmount = do - mergeUnclaimedRewards app - -- check target amount - [(_, unclaimed)] <- query @UnclaimedReward app.dso - unclaimed === UnclaimedReward with - dso = app.dso - amount = expectedAmount - - --- | Merge all unclaimed reward contracts. -mergeUnclaimedRewards : AmuletApp -> Script () -mergeUnclaimedRewards app = do - unclaimedRewards <- query @UnclaimedReward app.dso - when (length unclaimedRewards > 1) $ - void $ submitExerciseAmuletRulesByKey app [app.dso] [] - AmuletRules_MergeUnclaimedRewards with - unclaimedRewardCids = map fst unclaimedRewards diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/TestValidatorFaucet.daml b/daml/splice-amulet-test/daml/Splice/Scripts/TestValidatorFaucet.daml index 57de0f8be5..05c1bc1d6c 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/TestValidatorFaucet.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/TestValidatorFaucet.daml @@ -4,6 +4,7 @@ module Splice.Scripts.TestValidatorFaucet where import DA.Assert +import DA.List (head) import DA.Time import Daml.Script @@ -24,7 +25,8 @@ test_ValidatorFaucet = do map (._2) coupons === [ValidatorLivenessActivityRecord with dso = app.dso validator = aliceValidator.primaryParty - round] + round + weight = None] let checkActivityRecordsDoNotExist round = do [] <- queryFilter @ValidatorLivenessActivityRecord app.dso (\co -> co.round == round) @@ -102,3 +104,113 @@ test_ValidatorFaucet = do openRoundCid = round2._1 return () + +data ValidatorWeightTestParams = ValidatorWeightTestParams with + bobWeight : Decimal + aliceExpectedMin : Decimal + aliceExpectedMax : Decimal + bobExpectedMin : Decimal + bobExpectedMax : Decimal + deriving (Show, Eq) + +-- With weight 3.0, Bob should receive 3x Alice's rewards +test_ValidatorFaucetWithWeight3: Script () +test_ValidatorFaucetWithWeight3 = do + let bobWeight = 3.0 + aliceExpectedMin = 2.85 + aliceExpectedMax = 3.0 + testValidatorFaucetWithWeight_Helper ValidatorWeightTestParams with + bobWeight = bobWeight + aliceExpectedMin = aliceExpectedMin + aliceExpectedMax = aliceExpectedMax + bobExpectedMin = aliceExpectedMin * bobWeight + bobExpectedMax = aliceExpectedMax * bobWeight + +-- With weight 0.0, Bob should receive no rewards +test_ValidatorFaucetWithWeight0: Script () +test_ValidatorFaucetWithWeight0 = do + let bobWeight = 0.0 + aliceExpectedMin = 2.85 + aliceExpectedMax = 3.0 + testValidatorFaucetWithWeight_Helper ValidatorWeightTestParams with + bobWeight = bobWeight + aliceExpectedMin = aliceExpectedMin + aliceExpectedMax = aliceExpectedMax + bobExpectedMin = aliceExpectedMin * bobWeight + bobExpectedMax = aliceExpectedMax * bobWeight + +-- Test that runNextIssuance properly handles ValidatorLicense weights +-- in the optTotalValidatorLivenessActivityRecords calculation +-- Bob has been given a large weight to trigger the use of `cappedRewardsToIssue` (in computeIssuanceTranche) +-- which would result in alice getting lower rewards than the default value of about 2.85 +test_ValidatorLivenessWeightInRunNextIssuance : Script () +test_ValidatorLivenessWeightInRunNextIssuance = do + let bobWeight = 15000.0 + aliceExpectedMin = 2.5 + aliceExpectedMax = 2.55 + testValidatorFaucetWithWeight_Helper ValidatorWeightTestParams with + bobWeight = bobWeight + aliceExpectedMin = aliceExpectedMin + aliceExpectedMax = aliceExpectedMax + bobExpectedMin = aliceExpectedMin * bobWeight + bobExpectedMax = aliceExpectedMax * bobWeight + +-- Helper function to test validator faucet with different weights +-- Uses no-fee config to avoid balance changes due to fees +testValidatorFaucetWithWeight_Helper : ValidatorWeightTestParams -> Script () +testValidatorFaucetWithWeight_Helper params = do + DefaultAppWithUsers{..} <- setupDefaultAppWithUsersNoFees + + [(aliceLicenseCid, _)] <- query @ValidatorLicense aliceValidator.primaryParty + [(bobLicenseCid, bobLicense)] <- query @ValidatorLicense bobValidator.primaryParty + + -- Update bob's license to have the specified weight + submit app.dso $ archiveCmd bobLicenseCid + bobLicenseCid <- submitMulti [bobValidator.primaryParty, app.dso] [] $ createCmd bobLicense with + weight = Some params.bobWeight + + aliceBalanceBefore <- getNormalizedBalance aliceValidator.primaryParty + bobBalanceBefore <- getNormalizedBalance bobValidator.primaryParty + + -- Get the current open round + rounds <- getOpenRoundsSorted app + let (round0Cid, round0) = head rounds + + -- Both alice and bob record liveness activity + passTime (hours 1) + _ <- submitMulti [aliceValidator.primaryParty] [app.dso] $ exerciseCmd aliceLicenseCid ValidatorLicense_RecordValidatorLivenessActivity with + openRoundCid = round0Cid + + _ <- submitMulti [bobValidator.primaryParty] [app.dso] $ exerciseCmd bobLicenseCid ValidatorLicense_RecordValidatorLivenessActivity with + openRoundCid = round0Cid + + -- Verify the activity records were created with correct weights + aliceRecords <- queryFilter @ValidatorLivenessActivityRecord app.dso + (\r -> r.validator == aliceValidator.primaryParty && r.round == round0.round) + bobRecords <- queryFilter @ValidatorLivenessActivityRecord app.dso + (\r -> r.validator == bobValidator.primaryParty && r.round == round0.round) + + length aliceRecords === 1 + length bobRecords === 1 + + let [(_, aliceRecord)] = aliceRecords + let [(_, bobRecord)] = bobRecords + + aliceRecord.weight === None + bobRecord.weight === Some params.bobWeight + + runNextIssuance app + runNextIssuance app + + runAmuletDepositBots app + + aliceBalanceAfter <- getNormalizedBalance aliceValidator.primaryParty + bobBalanceAfter <- getNormalizedBalance bobValidator.primaryParty + + let aliceReward = aliceBalanceAfter - aliceBalanceBefore + let bobReward = bobBalanceAfter - bobBalanceBefore + + require "Alice should receive expected rewards" (aliceReward >= params.aliceExpectedMin && aliceReward <= params.aliceExpectedMax) + require "Bob should receive expected reward based on weight" (bobReward >= params.bobExpectedMin && bobReward <= params.bobExpectedMax) + + return () diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Issuance.daml b/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Issuance.daml index e32b7b3edb..bc6b27a4c8 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Issuance.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Issuance.daml @@ -53,7 +53,8 @@ summaryExample1 = OpenMiningRoundSummary with totalValidatorRewardCoupons = 10000.0 totalFeaturedAppRewardCoupons = 400.0 totalUnfeaturedAppRewardCoupons = 9600.0 - optTotalValidatorFaucetCoupons = Some 50 + optTotalValidatorFaucetCoupons = None + optTotalValidatorLivenessActivityRecords = Some 50.0 totalSvRewardWeight = 100 expectedParameters_E1_0_0p5 : IssuingRoundParameters @@ -130,7 +131,8 @@ summaryExamplNoActivity = OpenMiningRoundSummary with totalValidatorRewardCoupons = 0.0 totalFeaturedAppRewardCoupons = 0.0 totalUnfeaturedAppRewardCoupons = 0.0 - optTotalValidatorFaucetCoupons = Some 0 + optTotalValidatorFaucetCoupons = None + optTotalValidatorLivenessActivityRecords = Some 0.0 totalSvRewardWeight = 0 expectedParameters_NoActivity_0_0p5 : IssuingRoundParameters @@ -159,7 +161,8 @@ summaryExample3 = OpenMiningRoundSummary with totalValidatorRewardCoupons = 200.0 totalFeaturedAppRewardCoupons = 100.0 totalUnfeaturedAppRewardCoupons = 100.0 - optTotalValidatorFaucetCoupons = Some 50 + optTotalValidatorFaucetCoupons = None + optTotalValidatorLivenessActivityRecords = Some 50.0 totalSvRewardWeight = 2 @@ -190,7 +193,8 @@ summaryExampleLargeSvRewardWeight = OpenMiningRoundSummary with totalValidatorRewardCoupons = 0.0 totalFeaturedAppRewardCoupons = 0.0 totalUnfeaturedAppRewardCoupons = 0.0 - optTotalValidatorFaucetCoupons = Some 0 + optTotalValidatorFaucetCoupons = None + optTotalValidatorLivenessActivityRecords = Some 0.0 totalSvRewardWeight = 240 * 10000 -- total weight is 240 and it is represented in basis points expectedParameters_LargeSvRewardWeight_10plus : IssuingRoundParameters @@ -210,6 +214,3 @@ testLargeSvRewardWeight = script do let actual = computeIssuingRoundParameters tickDuration amuletPrice issuanceConfig_10plus summaryExampleLargeSvRewardWeight expectedParameters_LargeSvRewardWeight_10plus === actual - - - diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml b/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml index f7b47d47d6..0b0c01180d 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml @@ -23,13 +23,13 @@ import Splice.Api.Token.MetadataV1 import Splice.Amulet import Splice.Amulet.TokenApiUtils import Splice.AmuletRules -import Splice.AmuletConfig (AmuletConfig(..)) +import Splice.AmuletConfig (AmuletConfig(..), USD, TransferConfig(..)) import qualified Splice.AmuletConfig as Unit import Splice.Expiry import Splice.ExternalPartyAmuletRules import Splice.Issuance import Splice.Round -import Splice.Fees +import Splice.Fees (FixedFee(..), RatePerRound(..), SteppedRate(..), getValueAsOfRound0) import Splice.Types import Splice.RelRound import Splice.Schedule @@ -58,7 +58,11 @@ setupApp = genericSetupApp "" -- | Setup the DSO party with a specific prefix and the contracts defining the Amulet app. genericSetupApp : Text -> Script AmuletApp -genericSetupApp dsoPrefix = do +genericSetupApp dsoPrefix = genericSetupAppWithConfigSchedule dsoPrefix defaultAmuletConfigSchedule + +-- | Setup the DSO party with a specific prefix and custom config schedule. +genericSetupAppWithConfigSchedule : Text -> Schedule Time (AmuletConfig USD) -> Script AmuletApp +genericSetupAppWithConfigSchedule dsoPrefix configSchedule = do -- use a time that is easy to reason about in script outputs setTime demoTime @@ -70,7 +74,7 @@ genericSetupApp dsoPrefix = do recordValidatorOf app app.dso app.dso _ <- submit dso $ createCmd AmuletRules with - configSchedule = defaultAmuletConfigSchedule + configSchedule isDevNet = True .. @@ -84,6 +88,27 @@ genericSetupApp dsoPrefix = do -- return the off-ledger reference to the app for later script steps return app +-- | Setup the DSO party and the contracts defining the Amulet app with no fees. +setupAppNoFees : Script AmuletApp +setupAppNoFees = + let noFeeTransferConfig = TransferConfig with + createFee = FixedFee 0.0 + holdingFee = RatePerRound 0.0 + lockHolderFee = FixedFee 0.0 + transferFee = SteppedRate with + initialRate = 0.0 + steps = [] + extraFeaturedAppRewardAmount = 1.0 + maxNumInputs = 100 + maxNumOutputs = 100 + maxNumLockHolders = 10 + noFeeConfig = defaultAmuletConfig with + transferConfig = noFeeTransferConfig + noFeeSchedule = Schedule with + initialValue = noFeeConfig + futureValues = [] + in genericSetupAppWithConfigSchedule "" noFeeSchedule + -- AmuletApp users -------------- @@ -108,6 +133,8 @@ setupValidator' app name = do contactPoint = name <> "@example.com" lastUpdatedAt = now lastActiveAt = Some now + weight = None + kind = None userId <- validateUserId name -- TODO(#2957): remove read-as DSO party. @@ -159,9 +186,9 @@ data DefaultAppWithUsers = DefaultAppWithUsers with provider2Validator: AmuletUser deriving (Show, Eq, Ord) -setupDefaultAppWithUsers: Script DefaultAppWithUsers -setupDefaultAppWithUsers = do - app <- setupApp +setupDefaultAppWithUsersInternal: Script AmuletApp -> Script DefaultAppWithUsers +setupDefaultAppWithUsersInternal appSetup = do + app <- appSetup (alice, aliceValidator) <- setupSelfHostedUser app "alice" (bob, bobValidator) <- setupSelfHostedUser app "bob" charlie <- setupUser app "charlie" bobValidator.primaryParty @@ -169,6 +196,12 @@ setupDefaultAppWithUsers = do (provider2, provider2Validator) <- setupSelfHostedUser app "provider2" return DefaultAppWithUsers with .. +setupDefaultAppWithUsers: Script DefaultAppWithUsers +setupDefaultAppWithUsers = setupDefaultAppWithUsersInternal setupApp + +setupDefaultAppWithUsersNoFees: Script DefaultAppWithUsers +setupDefaultAppWithUsersNoFees = setupDefaultAppWithUsersInternal setupAppNoFees + -- dummy argument to avoid running this script by default. getAllValidatorRights : AmuletApp -> Script [ValidatorRight] getAllValidatorRights _app = do @@ -263,7 +296,8 @@ runNextIssuanceInternal app amuletPrice = do totalFeaturedAppRewardCoupons = sum [ c.amount | (_, c) <- appRewardCoupons, c.featured] totalUnfeaturedAppRewardCoupons = sum [ c.amount | (_, c) <- appRewardCoupons, not (c.featured)] totalSvRewardWeight = sum [ c.weight | (_, c) <- svRewardCoupons] - optTotalValidatorFaucetCoupons = Some (length validatorFaucetCoupons + length validatorLivenessActivityRecords) + optTotalValidatorFaucetCoupons = None + optTotalValidatorLivenessActivityRecords = Some (intToDecimal (length validatorFaucetCoupons) + sum [ fromOptional defaultValidatorLicenseWeight c.weight | (_, c) <- validatorLivenessActivityRecords ]) -- create issuing mining round submitExerciseAmuletRulesByKey app [app.dso] [] AmuletRules_MiningRound_StartIssuing with diff --git a/daml/splice-amulet/daml.yaml b/daml/splice-amulet/daml.yaml index d0edcb5cbb..72d3f38f18 100644 --- a/daml/splice-amulet/daml.yaml +++ b/daml/splice-amulet/daml.yaml @@ -6,7 +6,7 @@ sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 name: splice-amulet source: daml -version: 0.1.14 +version: 0.1.15 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-amulet/daml/Splice/AmuletRules.daml b/daml/splice-amulet/daml/Splice/AmuletRules.daml index 392e25fa9f..9e068064e6 100644 --- a/daml/splice-amulet/daml/Splice/AmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/AmuletRules.daml @@ -500,6 +500,7 @@ template AmuletRules return AmuletRules_MiningRound_ArchiveResult -- Batch expiry of unclaimed rewards for a specific claimed round + -- __Deprecated__ use `DsoRules_ClaimExpiredRewards` instead. nonconsuming choice AmuletRules_ClaimExpiredRewards : AmuletRules_ClaimExpiredRewardsResult with closedRoundCid : ContractId ClosedMiningRound @@ -510,54 +511,7 @@ template AmuletRules optValidatorLivenessActivityRecordCids : Optional [ContractId ValidatorLivenessActivityRecord] controller dso do - -- retrieve round - closedRound <- fetchReferenceData (ForDso with dso) closedRoundCid - - -- expire validator rewards and compute their unclaimed rewards - expiredValidatorRewards <- forA validatorRewardCouponCids $ \validatorRewardCouponCid -> do - expireResult <- exercise validatorRewardCouponCid ValidatorRewardCoupon_DsoExpire with closedRoundCid - return expireResult.amount - - let validatorRewards = sum expiredValidatorRewards - - -- expire validator faucet coupons and compute their unclaimed rewards - forA_ (fromOptional [] optValidatorFaucetCouponCids) $ \validatorFaucetCouponCid -> do - exercise validatorFaucetCouponCid ValidatorFaucetCoupon_DsoExpire with closedRoundCid - - let numValidatorFaucetCoupons = optional 0.0 (intToDecimal . length) optValidatorFaucetCouponCids - - -- expire validator liveness activity records and compute their unclaimed rewards - forA_ (fromOptional [] optValidatorLivenessActivityRecordCids) $ \validatorLivenessActivityRecordCid -> do - exercise validatorLivenessActivityRecordCid ValidatorLivenessActivityRecord_DsoExpire with closedRoundCid - - let numValidatorLivenessActivityRecords = optional 0.0 (intToDecimal . length) optValidatorLivenessActivityRecordCids - - -- expire app rewards and compute their unclaimed rewards - expiredAppRewards <- forA appCouponCids $ \appCouponCid -> do - expireResult <- exercise appCouponCid AppRewardCoupon_DsoExpire with closedRoundCid - return (expireResult.featured, expireResult.amount) - - let featuredAppRewards = sum [ amount | (featured, amount) <- expiredAppRewards, featured ] - let unfeaturedAppRewards = sum [ amount | (featured, amount) <- expiredAppRewards, not featured ] - - -- expire validator rewards and compute their unclaimed rewards - expiredSvRewards <- forA svRewardCouponCids $ \svRewardCouponCid -> do - expireResult <- exercise svRewardCouponCid SvRewardCoupon_DsoExpire with closedRoundCid - return expireResult.weight - - let svRewardsWeight = sum expiredSvRewards - - -- create unclaimed reward for the total - let amount = - validatorRewards * closedRound.issuancePerValidatorRewardCoupon + - (numValidatorFaucetCoupons + numValidatorLivenessActivityRecords) * getClosedMiningRoundIssuancePerValidatorFaucetCoupon closedRound + - featuredAppRewards * closedRound.issuancePerFeaturedAppRewardCoupon + - unfeaturedAppRewards * closedRound.issuancePerUnfeaturedAppRewardCoupon + - intToDecimal svRewardsWeight * closedRound.issuancePerSvRewardCoupon - unclaimedRewardCid <- if (amount <= 0.0) - then pure None - else Some <$> create UnclaimedReward with dso; amount - return AmuletRules_ClaimExpiredRewardsResult with unclaimedRewardCid + deprecatedChoice "splice-amulet" "0.1.21" "AmuletRules_ClaimExpiredRewards" -- Batch merge of unclaimed rewards nonconsuming choice AmuletRules_MergeUnclaimedRewards : AmuletRules_MergeUnclaimedRewardsResult @@ -931,7 +885,7 @@ summarizeAndConsumeInputs csum dso sender inps = do totalValidatorRewardAmount = s.totalValidatorRewardAmount totalUnclaimedActivityRecordAmount = s.totalUnclaimedActivityRecordAmount totalValidatorFaucetAmount = - s.totalValidatorFaucetAmount + getIssuingMiningRoundIssuancePerValidatorFaucetCoupon miningRound + s.totalValidatorFaucetAmount + getIssuingMiningRoundIssuancePerValidatorFaucetCoupon miningRound * fromOptional defaultValidatorLicenseWeight record.weight totalSvRewardAmount = s.totalSvRewardAmount totalHoldingFees = s.totalHoldingFees amountArchivedAsOfRoundZero = s.amountArchivedAsOfRoundZero diff --git a/daml/splice-amulet/daml/Splice/Issuance.daml b/daml/splice-amulet/daml/Splice/Issuance.daml index 91da289785..8d7421bc7e 100644 --- a/daml/splice-amulet/daml/Splice/Issuance.daml +++ b/daml/splice-amulet/daml/Splice/Issuance.daml @@ -57,11 +57,17 @@ data OpenMiningRoundSummary = OpenMiningRoundSummary with totalUnfeaturedAppRewardCoupons : Decimal totalSvRewardWeight : Int optTotalValidatorFaucetCoupons : Optional Int - -- ^ Introduced as part of CIP-3. + -- ^ Introduced as part of CIP-3. Deprecated in favor of `optTotalValidatorLivenessActivityRecords` + optTotalValidatorLivenessActivityRecords : Optional Decimal -- ^ Replaces `optTotalValidatorFaucetCoupons` deriving (Eq, Show) -getTotalValidatorFaucetCoupons : OpenMiningRoundSummary -> Int -getTotalValidatorFaucetCoupons = fromOptional 0 . (.optTotalValidatorFaucetCoupons) +getTotalValidatorFaucetCoupons : OpenMiningRoundSummary -> Decimal +getTotalValidatorFaucetCoupons OpenMiningRoundSummary{optTotalValidatorLivenessActivityRecords, optTotalValidatorFaucetCoupons} = + case (optTotalValidatorLivenessActivityRecords, optTotalValidatorFaucetCoupons) of + (Some _, Some _) -> error "Only optTotalValidatorLivenessActivityRecords or optTotalValidatorFaucetCoupons must be set but not both" + (Some a, None) -> a + (None, Some a) -> intToDecimal a + (None, None) -> 0.0 -- | Parameters to use in a round that issues amulet as rewards for collected coupons. data IssuingRoundParameters = IssuingRoundParameters with @@ -80,7 +86,7 @@ validateOpenMiningRoundSummary summary = do require "totalValidatorRewardCoupons >= 0.0" (summary.totalValidatorRewardCoupons >= 0.0) require "totalFeaturedAppRewardCoupons >= 0.0" (summary.totalFeaturedAppRewardCoupons >= 0.0) require "totalUnfeaturedAppRewardCoupons >= 0.0" (summary.totalUnfeaturedAppRewardCoupons >= 0.0) - require "totalValidatorFaucetCoupons >= 0" (getTotalValidatorFaucetCoupons summary >= 0) + require "totalValidatorFaucetCoupons >= 0" (getTotalValidatorFaucetCoupons summary >= 0.0) computeIssuingRoundParameters : RelTime -> Decimal -> IssuanceConfig -> OpenMiningRoundSummary -> IssuingRoundParameters computeIssuingRoundParameters tickDuration amuletPrice config summary = @@ -123,7 +129,7 @@ computeIssuingRoundParameters tickDuration amuletPrice config summary = validatorFaucetIssuance = computeIssuanceTranche validatorRewardIssuance.unclaimedRewards (getValidatorFaucetCap config / amuletPrice) - (intToDecimal $ getTotalValidatorFaucetCoupons summary) + (getTotalValidatorFaucetCoupons summary) unfeaturedAppIssuance = computeIssuanceTranche (amuletsToIssueInRound * config.appRewardPercentage) diff --git a/daml/splice-amulet/daml/Splice/ValidatorLicense.daml b/daml/splice-amulet/daml/Splice/ValidatorLicense.daml index deefcb2db0..11efed84dd 100644 --- a/daml/splice-amulet/daml/Splice/ValidatorLicense.daml +++ b/daml/splice-amulet/daml/Splice/ValidatorLicense.daml @@ -12,6 +12,9 @@ import Splice.Round import Splice.Types import Splice.Util +data LicenseKind = OperatorLicense | NonOperatorLicense + deriving (Eq, Show) + data FaucetState = FaucetState with firstReceivedFor : Round -- ^ The first round for which a coupon was received. lastReceivedFor : Round -- ^ The last round for which a coupon was received. @@ -56,6 +59,8 @@ template ValidatorLicense with faucetState : Optional FaucetState metadata : Optional ValidatorLicenseMetadata lastActiveAt : Optional Time -- ^ Last time this validator was active. Tracked to get a view on the set of validator nodes that are up and running. + weight : Optional Decimal -- ^ Weight of the activity records produced by this license. Determined through SV votes. If not set, weight is 1.0 + kind : Optional LicenseKind -- ^ Type of license. If not set, kind is OperatorLicense where signatory dso -- sponsor is not a signatory as that complicates re-issuing crates observer validator -- not a signatory to simplify the creation of the license as part of onboarding @@ -99,6 +104,7 @@ template ValidatorLicense with dso validator round = openRound.round + weight return ValidatorLicense_RecordValidatorLivenessActivityResult with .. @@ -147,6 +153,8 @@ template ValidatorLicense with lastActiveAt = Some now pure (ValidatorLicense_ReportActiveResult licenseCid) +defaultValidatorLicenseWeight : Decimal +defaultValidatorLicenseWeight = 1.0 metadataUpdateMinInterval : RelTime metadataUpdateMinInterval = hours 1 @@ -198,6 +206,7 @@ template ValidatorLivenessActivityRecord with dso : Party validator : Party round : Round + weight : Optional Decimal where signatory dso observer validator diff --git a/daml/splice-dso-governance-test/daml.yaml b/daml/splice-dso-governance-test/daml.yaml index 900384cfb7..e70244196f 100644 --- a/daml/splice-dso-governance-test/daml.yaml +++ b/daml/splice-dso-governance-test/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 name: splice-dso-governance-test source: daml -version: 0.1.23 +version: 0.1.24 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml index 17abe747c3..e6efe45e79 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml @@ -9,6 +9,7 @@ import DA.Assert import DA.Foldable (forA_) import DA.List import qualified DA.Map as Map +import DA.Optional (fromOptional) import qualified DA.Set as Set import qualified DA.Text as T import Daml.Script @@ -264,7 +265,8 @@ runNextIssuanceD app amuletPrice = do totalFeaturedAppRewardCoupons = sum [ c.amount | (_, c) <- appRewardCoupons, c.featured] totalUnfeaturedAppRewardCoupons = sum [ c.amount | (_, c) <- appRewardCoupons, not (c.featured)] totalSvRewardWeight = sum [ c.weight | (_, c) <- svRewardCoupons] - optTotalValidatorFaucetCoupons = Some (length validatorFaucetCoupons + length validatorLivenessActivityRecords) + optTotalValidatorFaucetCoupons = None + optTotalValidatorLivenessActivityRecords = Some (intToDecimal (length validatorFaucetCoupons) + sum [ fromOptional defaultValidatorLicenseWeight c.weight | (_, c) <- validatorLivenessActivityRecords ]) -- create issuing mining round submitExerciseAmuletRulesByKey app [app.dso] [] AmuletRules_MiningRound_StartIssuing with diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestClaimExpiredRewards.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestClaimExpiredRewards.daml new file mode 100644 index 0000000000..2994e5568b --- /dev/null +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestClaimExpiredRewards.daml @@ -0,0 +1,250 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +module Splice.Scripts.TestClaimExpiredRewards where + +import DA.Action (void, when) +import DA.Assert + +import Daml.Script + +import Splice.Amulet +import Splice.AmuletRules +import Splice.DsoRules +import Splice.Issuance +import Splice.Round +import Splice.ValidatorLicense +import Splice.Scripts.Util +import Splice.Scripts.DsoTestUtils +import Splice.Testing.Registries.AmuletRegistry.Parameters + +-- Note: This tests DsoRules_ClaimExpiredRewards +-- which is identical to AmuletRules_ClaimExpiredRewards +-- Hence below tests have been kept mostly identical to the one in +-- Splice.Scripts.TestUnclaimedRewards + + +totalRewardsPerRound : Decimal +totalRewardsPerRound = issuanceConfig_0_0p5.amuletToIssuePerYear / (365.0 * 24.0 * 6.0) -- one round every 10' + +testClaimExpiredRewards_allCouponTypes : Script () +testClaimExpiredRewards_allCouponTypes = do + (app, _, (sv1, _, _, _)) <- initDevNet + + alice <- allocateParty "alice" + provider1 <- allocateParty "provider1" + provider2 <- allocateParty "provider2" + + -- Get the first open round to create coupons for + [round0, _, _] <- getActiveOpenRoundsSorted app + let roundNumber = round0._2.round + + -- bare-create all three kinds of rewards + appCoupon1 <- submitMulti [app.dso, provider1] [] $ createCmd AppRewardCoupon with + dso = app.dso + provider = provider1 + beneficiary = None + amount = 5.7 + round = roundNumber + featured = True + + appCoupon2 <- submitMulti [app.dso, provider1] [] $ createCmd AppRewardCoupon with + dso = app.dso + provider = provider1 + beneficiary = None + amount = 3.3 + round = roundNumber + featured = False + + validatorCoupon1 <- submitMulti [app.dso, alice] [] $ createCmd ValidatorRewardCoupon with + dso = app.dso + user = alice + amount = 2.3 + round = roundNumber + + validatorFaucet1 <- submitMulti [app.dso, alice] [] $ createCmd ValidatorFaucetCoupon with + dso = app.dso + validator = alice + round = roundNumber + + validatorFaucet2 <- submitMulti [app.dso, provider1] [] $ createCmd ValidatorFaucetCoupon with + dso = app.dso + validator = provider1 + round = roundNumber + + validatorLivenessActivityRecord1 <- submitMulti [app.dso] [] $ createCmd ValidatorLivenessActivityRecord with + dso = app.dso + validator = alice + round = roundNumber + weight = None + + validatorLivenessActivityRecord2 <- submitMulti [app.dso] [] $ createCmd ValidatorLivenessActivityRecord with + dso = app.dso + validator = provider1 + round = roundNumber + weight = None + + svRewardCoupon1 <- submitMulti [app.dso, provider2] [] $ createCmd SvRewardCoupon with + dso = app.dso + sv = provider2 + beneficiary = provider2 + round = roundNumber + weight = 10 + + -- start issuing + runNextIssuance app + runAmuletDepositBots app + + [(_, issuing)] <- query @IssuingMiningRound app.dso + + -- check size of unclaimed reward issuance + let allocatedRewards = + 5.7 * issuanceConfig_0_0p5.featuredAppRewardCap + + 3.3 * issuanceConfig_0_0p5.unfeaturedAppRewardCap + + 2.3 * issuanceConfig_0_0p5.validatorRewardCap + + 2.0 * getValidatorFaucetCap issuanceConfig_0_0p5 + + 2.0 * getValidatorFaucetCap issuanceConfig_0_0p5 + + 10.0 * issuing.issuancePerSvRewardCoupon + mergeAndCheckUnclaimedRewardAmount app (totalRewardsPerRound - allocatedRewards) + + -- run another two issuances + runNextIssuance app + runNextIssuance app + + let numIssuingRoundsToDate = 3.0 + mergeAndCheckUnclaimedRewardAmount app (numIssuingRoundsToDate * totalRewardsPerRound - allocatedRewards) + + -- run another issuance such that round 0 becomes closed + runNextIssuance app + + -- collect the reward coupons + [(closedRoundCid, _)] <- queryFilter @ClosedMiningRound app.dso (\r -> r.round == roundNumber) + + let collect = AmuletRules_ClaimExpiredRewards with + closedRoundCid + validatorRewardCouponCids = [validatorCoupon1] + appCouponCids = [appCoupon1, appCoupon2] + svRewardCouponCids = [svRewardCoupon1] + optValidatorFaucetCouponCids = Some [validatorFaucet1, validatorFaucet2] + optValidatorLivenessActivityRecordCids = Some [validatorLivenessActivityRecord1, validatorLivenessActivityRecord2] + + [(amuletRulesCid, _)] <- query @AmuletRules app.dso + + dsoDelegateSubmits app $ \cid -> exerciseCmd cid $ + DsoRules_ClaimExpiredRewards with + amuletRulesCid + choiceArg = collect + sv = Some sv1 + + -- check that the addition is done correctly + let numIssuingRoundsToDate = 4.0 + -- note that now the reward coupons have been collected as well, and we thus + -- don't have to subtract the allocatedRewards anymore. They've been merged back + -- into the `UnclaimedReward`s fund. + mergeAndCheckUnclaimedRewardAmount app (numIssuingRoundsToDate * totalRewardsPerRound) + + -- Also verify that the coupons have been archived + validatorCoupons <- queryFilter @ValidatorRewardCoupon app.dso (\r -> r.round == roundNumber) + validatorCoupons === [] + + appCoupons <- queryFilter @AppRewardCoupon app.dso (\r -> r.round == roundNumber) + appCoupons === [] + + svCoupons <- queryFilter @SvRewardCoupon app.dso (\r -> r.round == roundNumber) + svCoupons === [] + + validatorFaucets <- queryFilter @ValidatorFaucetCoupon app.dso (\r -> r.round == roundNumber) + validatorFaucets === [] + + validatorLivenessRecords <- queryFilter @ValidatorLivenessActivityRecord app.dso (\r -> r.round == roundNumber) + validatorLivenessRecords === [] + + pure () + +testClaimExpiredRewards_validatorLicenseNonDefaultWeight : Script () +testClaimExpiredRewards_validatorLicenseNonDefaultWeight = do + (app, _, (sv1, _, _, _)) <- initDevNet + + alice <- allocateParty "alice" + provider1 <- allocateParty "provider1" + provider2 <- allocateParty "provider2" + + -- Get the first open round to create coupons for + [round0, _, _] <- getActiveOpenRoundsSorted app + let roundNumber = round0._2.round + + -- Create validator liveness activity records with different weights + validatorLivenessActivityRecord1 <- submitMulti [app.dso] [] $ createCmd ValidatorLivenessActivityRecord with + dso = app.dso + validator = alice + round = roundNumber + weight = Some 2.5 + + validatorLivenessActivityRecord2 <- submitMulti [app.dso] [] $ createCmd ValidatorLivenessActivityRecord with + dso = app.dso + validator = provider1 + round = roundNumber + weight = Some 0.5 + + validatorLivenessActivityRecord3 <- submitMulti [app.dso] [] $ createCmd ValidatorLivenessActivityRecord with + dso = app.dso + validator = provider2 + round = roundNumber + weight = None -- Default weight (1.0) + + -- Start issuing + runNextIssuance app + runAmuletDepositBots app + + -- Calculate expected allocated rewards with weights + let weightedValidatorLivenessRewards = + (2.5 + 0.5 + 1.0) * getValidatorFaucetCap issuanceConfig_0_0p5 + let allocatedRewards = weightedValidatorLivenessRewards + mergeAndCheckUnclaimedRewardAmount app (totalRewardsPerRound - allocatedRewards) + + -- Run issuance to close round 0 + runNextIssuance app + runNextIssuance app + runNextIssuance app + + -- Collect the reward coupons + [(closedRoundCid, _)] <- queryFilter @ClosedMiningRound app.dso (\r -> r.round == roundNumber) + let collect = AmuletRules_ClaimExpiredRewards with + closedRoundCid + validatorRewardCouponCids = [] + appCouponCids = [] + svRewardCouponCids = [] + optValidatorFaucetCouponCids = None + optValidatorLivenessActivityRecordCids = Some [validatorLivenessActivityRecord1, validatorLivenessActivityRecord2, validatorLivenessActivityRecord3] + + [(amuletRulesCid, _)] <- query @AmuletRules app.dso + + dsoDelegateSubmits app $ \cid -> exerciseCmd cid $ + DsoRules_ClaimExpiredRewards with + amuletRulesCid + choiceArg = collect + sv = Some sv1 + + -- Check that the weighted addition is done correctly + let numIssuingRoundsToDate = 4.0 + mergeAndCheckUnclaimedRewardAmount app (numIssuingRoundsToDate * totalRewardsPerRound) + pure () + +mergeAndCheckUnclaimedRewardAmount : AmuletApp -> Decimal -> Script () +mergeAndCheckUnclaimedRewardAmount app expectedAmount = do + mergeUnclaimedRewards app + -- check target amount + [(_, unclaimed)] <- query @UnclaimedReward app.dso + unclaimed === UnclaimedReward with + dso = app.dso + amount = expectedAmount + + +-- | Merge all unclaimed reward contracts. +mergeUnclaimedRewards : AmuletApp -> Script () +mergeUnclaimedRewards app = do + unclaimedRewards <- query @UnclaimedReward app.dso + when (length unclaimedRewards > 1) $ + void $ submitExerciseAmuletRulesByKey app [app.dso] [] + AmuletRules_MergeUnclaimedRewards with + unclaimedRewardCids = map fst unclaimedRewards diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestOnboarding.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestOnboarding.daml index 2c43ac9bc4..d6397f8cbf 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestOnboarding.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestOnboarding.daml @@ -9,7 +9,6 @@ import DA.Optional (fromSome) import qualified DA.Map as Map import Daml.Script import DA.Time -import DA.List import Splice.ValidatorLicense @@ -48,6 +47,8 @@ testOnboardValidator = do contactPoint = "v1@example.com" lastUpdatedAt = now lastActiveAt = Some now + weight = None + kind = Some OperatorLicense -- so v1 is onboarded! @@ -78,6 +79,8 @@ testOnboardValidator = do version = "0.2.0" contactPoint = "v1@example2.com" lastActiveAt = Some metadataUpdateTime + weight = None + kind = Some OperatorLicense -- activity cannot be reported without advancing time @@ -98,6 +101,8 @@ testOnboardValidator = do version = "0.2.0" contactPoint = "v1@example2.com" lastActiveAt = Some now + weight = None + kind = Some OperatorLicense pure () testSvOnboarding : Script () @@ -473,66 +478,6 @@ testSvReonboarding = do pure () --- Ensure that duplicated ValidatorLicence contracts are merged -test_MergeValidatorLicense : Script () -test_MergeValidatorLicense = do - (_, dso, (sv1, sv2, _, _)) <- initDevNet - - -- a new validator prepares his participant - v1 <- allocateParty "v1" - now <- getTime - - validatorLicense1Cid <- submit dso $ createCmd ValidatorLicense with - validator = v1 - sponsor = sv1 - dso - faucetState = Some FaucetState with - firstReceivedFor = Round 0 - lastReceivedFor = Round 1 - numCouponsMissed = 0 - metadata = Some ValidatorLicenseMetadata with - version = "0.1.0" - contactPoint = "v1@example.com" - lastUpdatedAt = now - lastActiveAt = Some now - - validatorLicense2Cid <- submit dso $ createCmd ValidatorLicense with - validator = v1 - sponsor = sv2 - dso - faucetState = Some FaucetState with - firstReceivedFor = Round 0 - lastReceivedFor = Round 2 - numCouponsMissed = 0 - metadata = Some ValidatorLicenseMetadata with - version = "0.1.0" - contactPoint = "v1@example.com" - lastUpdatedAt = now - lastActiveAt = Some now - - -- there are two validator licenses for v1 - validatorLicenses <- queryFilter @ValidatorLicense dso (\license -> license.validator == v1) - length validatorLicenses === 2 - - -- the license with the highest lastReceivedFor is kept - [(rulesCid, _)] <- query @DsoRules dso - DsoRules_MergeValidatorLicenseResult validatorLicenseCid3 <- submitMulti [sv1] [dso] $ exerciseCmd rulesCid (DsoRules_MergeValidatorLicense [validatorLicense1Cid, validatorLicense2Cid] (Some sv1)) - validatorLicenses <- queryFilter @ValidatorLicense dso (\license -> license.validator == v1) - length validatorLicenses === 1 - (head validatorLicenses)._2.faucetState === Some FaucetState with - firstReceivedFor = Round 0 - lastReceivedFor = Round 2 - numCouponsMissed = 0 - - -- we can't merge a single contract - submitMultiMustFail [sv1] [dso] $ exerciseCmd rulesCid (DsoRules_MergeValidatorLicense [validatorLicenseCid3] (Some sv1)) - - -- we can't merge contracts for different validator names - [(validatorLicenseSv2Cid, _)] <- queryFilter @ValidatorLicense dso (\license -> license.validator == sv2) - submitMultiMustFail [sv1] [dso] $ exerciseCmd rulesCid (DsoRules_MergeValidatorLicense [validatorLicenseCid3, validatorLicenseSv2Cid] (Some sv1)) - - pure() - testBootstrapDevNetWithNonZeroRound : Script () testBootstrapDevNetWithNonZeroRound = do (app, _, _) <- initDecentralizedSynchronizerWithNonZeroRound True 34 diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestValidatorLicense.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestValidatorLicense.daml new file mode 100644 index 0000000000..c4117c58db --- /dev/null +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestValidatorLicense.daml @@ -0,0 +1,197 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +module Splice.Scripts.TestValidatorLicense where + +import DA.Assert +import DA.Action (unless) +import DA.List (head) +import Daml.Script +import DA.Time + +import Splice.ValidatorLicense +import Splice.DsoRules +import Splice.Scripts.DsoTestUtils +import Splice.Scripts.Util +import Splice.Types + + +testGrantValidatorLicense : Script () +testGrantValidatorLicense = do + (app, dso, (_, sv2, _, _)) <- initMainNet + + -- a new validator prepares his participant + v1 <- allocateParty "v1" + + now <- getTime + + -- sv2 grants a validator license to v1 + result <- svSubmits app sv2 $ \cid -> exerciseCmd cid $ + DsoRules_GrantValidatorLicense with + sponsor = sv2 + validator = v1 + + -- v1 received a validator license + [(validatorLicenseCid, validatorLicense)] <- queryFilter @ValidatorLicense dso (\license -> license.validator == v1) + validatorLicense === ValidatorLicense with + validator = v1 + sponsor = sv2 + dso + faucetState = None + metadata = None + lastActiveAt = Some now + weight = None + kind = Some NonOperatorLicense + + result.validatorLicense === validatorLicenseCid + + -- v1 can report activity + passTime (hours 1) + activityTime <- getTime + + submitMulti [v1] [dso] $ exerciseCmd validatorLicenseCid ValidatorLicense_ReportActive + + [(_, validatorLicense)] <- queryFilter @ValidatorLicense dso (\license -> license.validator == v1) + validatorLicense === ValidatorLicense with + validator = v1 + sponsor = sv2 + dso + faucetState = None + metadata = None + lastActiveAt = Some activityTime + weight = None + kind = Some NonOperatorLicense + + pure () + +testMergeValidatorLicense : Script () +testMergeValidatorLicense = do + (_, dso, (sv1, sv2, _, _)) <- initDevNet + + v1 <- allocateParty "v1" + now <- getTime + + validatorLicense1Cid <- submit dso $ createCmd ValidatorLicense with + validator = v1 + sponsor = sv1 + dso + faucetState = Some FaucetState with + firstReceivedFor = Round 0 + lastReceivedFor = Round 1 + numCouponsMissed = 0 + metadata = Some ValidatorLicenseMetadata with + version = "0.1.0" + contactPoint = "v1@example.com" + lastUpdatedAt = now + lastActiveAt = Some now + weight = None + kind = None + + + validatorLicense2Cid <- submit dso $ createCmd ValidatorLicense with + validator = v1 + sponsor = sv2 + dso + faucetState = Some FaucetState with + firstReceivedFor = Round 0 + lastReceivedFor = Round 2 + numCouponsMissed = 0 + metadata = Some ValidatorLicenseMetadata with + version = "0.1.0" + contactPoint = "v1@example.com" + lastUpdatedAt = now + lastActiveAt = Some now + weight = None + kind = None + + validatorLicenses <- queryFilter @ValidatorLicense dso (\license -> license.validator == v1) + length validatorLicenses === 2 + + -- the license with the highest lastReceivedFor is kept + [(rulesCid, _)] <- query @DsoRules dso + DsoRules_MergeValidatorLicenseResult _ <- submitMulti [sv1] [dso] $ exerciseCmd rulesCid (DsoRules_MergeValidatorLicense [validatorLicense1Cid, validatorLicense2Cid] (Some sv1)) + validatorLicenses <- queryFilter @ValidatorLicense dso (\license -> license.validator == v1) + length validatorLicenses === 1 + (head validatorLicenses)._2.faucetState === Some FaucetState with + firstReceivedFor = Round 0 + lastReceivedFor = Round 2 + numCouponsMissed = 0 + + pure() + +-- Test that runNextIssuanceD properly handles ValidatorLicense weights +-- in the optTotalValidatorLivenessActivityRecords calculation +-- Bob has been given a large weight to trigger the use of `cappedRewardsToIssue` (in computeIssuanceTranche) +-- which would result in alice getting lower rewards than the default value of about 2.85 +test_ValidatorLivenessWeightInRunNextIssuanceD : Script () +test_ValidatorLivenessWeightInRunNextIssuanceD = do + let bobWeight = 15000.0 + aliceExpectedMin = 2.5 + aliceExpectedMax = 2.55 + bobExpectedMin = aliceExpectedMin * bobWeight + bobExpectedMax = aliceExpectedMax * bobWeight + + (app, _, (_, _, _, _)) <- initDevNet + + -- Setup alice and bob + aliceValidator <- setupValidator' app "v_alice" + _ <- setupUser app "alice" aliceValidator.primaryParty + bobValidator <- setupValidator' app "v_bob" + _ <- setupUser app "bob" bobValidator.primaryParty + + [(aliceLicenseCid, _)] <- query @ValidatorLicense aliceValidator.primaryParty + [(bobLicenseCidInitial, bobLicense)] <- query @ValidatorLicense bobValidator.primaryParty + + -- Update bob license to have specified weight + submit app.dso $ archiveCmd bobLicenseCidInitial + bobLicenseCid <- submitMulti [bobValidator.primaryParty, app.dso] [] $ createCmd bobLicense with + weight = Some bobWeight + + aliceBalanceBefore <- getNormalizedBalance aliceValidator.primaryParty + bobBalanceBefore <- getNormalizedBalance bobValidator.primaryParty + + -- Get the current open round + rounds <- getOpenRoundsSorted app + let (round0Cid, round0) = head rounds + + -- Both record liveness activity + passTime (hours 1) + _ <- submitMulti [aliceValidator.primaryParty] [app.dso] $ exerciseCmd aliceLicenseCid ValidatorLicense_RecordValidatorLivenessActivity with + openRoundCid = round0Cid + + _ <- submitMulti [bobValidator.primaryParty] [app.dso] $ exerciseCmd bobLicenseCid ValidatorLicense_RecordValidatorLivenessActivity with + openRoundCid = round0Cid + + -- Verify the activity records were created with correct weights + aliceRecords <- queryFilter @ValidatorLivenessActivityRecord app.dso + (\r -> r.validator == aliceValidator.primaryParty && r.round == round0.round) + bobRecords <- queryFilter @ValidatorLivenessActivityRecord app.dso + (\r -> r.validator == bobValidator.primaryParty && r.round == round0.round) + + length aliceRecords === 1 + length bobRecords === 1 + + let [(_, aliceRecord)] = aliceRecords + let [(_, bobRecord)] = bobRecords + + aliceRecord.weight === None + bobRecord.weight === Some bobWeight + + runNextIssuanceD app 1.0 + runNextIssuanceD app 1.0 + + runAmuletDepositBots app + + aliceBalanceAfter <- getNormalizedBalance aliceValidator.primaryParty + bobBalanceAfter <- getNormalizedBalance bobValidator.primaryParty + + let aliceReward = aliceBalanceAfter - aliceBalanceBefore + let bobReward = bobBalanceAfter - bobBalanceBefore + + unless (aliceReward >= aliceExpectedMin && aliceReward <= aliceExpectedMax) $ + abort ("Alice should receive reward lower than the default 2.85. Expected within: [" <> show aliceExpectedMin <> ", " <> show aliceExpectedMax <> "], Got: " <> show aliceReward) + + unless (bobReward >= bobExpectedMin && bobReward <= bobExpectedMax) $ + abort ("Bob should receive ~15000 times rewards. Expected within: [" <> show bobExpectedMin <> ", " <> show bobExpectedMax <> "], Got: " <> show bobReward) + + return () diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/UnitTests/MergeValidatorLicense.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/UnitTests/MergeValidatorLicense.daml new file mode 100644 index 0000000000..9e690e5d2e --- /dev/null +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/UnitTests/MergeValidatorLicense.daml @@ -0,0 +1,171 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +module Splice.Scripts.UnitTests.MergeValidatorLicense where + +import DA.Assert +import Daml.Script + +import Splice.ValidatorLicense +import Splice.DsoRules.Utils +import Splice.Types (Round(..)) + +-- Weight merging: minimum weight is selected from non-None values +test_WeightMerging : Script () +test_WeightMerging = do + dso <- allocateParty "dso" + validator <- allocateParty "validator" + sponsor <- allocateParty "sponsor" + now <- getTime + + let createLicense : Optional Decimal -> Round -> ValidatorLicense + createLicense weight lastReceivedFor = + ValidatorLicense with + validator + sponsor + dso + faucetState = Some FaucetState with + firstReceivedFor = Round 0 + lastReceivedFor + numCouponsMissed = 0 + metadata = None + lastActiveAt = Some now + weight + kind = Some OperatorLicense + + -- Scenario 1: Minimum among some weights + let license1a = createLicense None (Round 1) + let license2a = createLicense (Some 2.0) (Round 2) + let license3a = createLicense (Some 1.5) (Round 3) + let Right mergedLicense1 = mergeValidatorLicenses [license1a, license2a, license3a] + mergedLicense1.weight === Some 1.5 + + -- Scenario 2: All None weights + let license1b = createLicense None (Round 1) + let license2b = createLicense None (Round 2) + let Right mergedLicense2 = mergeValidatorLicenses [license1b, license2b] + mergedLicense2.weight === None + +-- kind merging: `None` kind is same as `Some OperatorLicense` +-- So OperatorLicense takes precedence over NonOperatorLicense +test_KindMerging : Script () +test_KindMerging = do + dso <- allocateParty "dso" + validator <- allocateParty "validator" + sponsor <- allocateParty "sponsor" + now <- getTime + + let createLicense : Optional LicenseKind -> Round -> ValidatorLicense + createLicense kind lastReceivedFor = + ValidatorLicense with + validator + sponsor + dso + faucetState = Some FaucetState with + firstReceivedFor = Round 0 + lastReceivedFor + numCouponsMissed = 0 + metadata = None + lastActiveAt = Some now + weight = None + kind + + -- Scenario 1: None + NonOperator → Operator + let license1a = createLicense None (Round 1) + let license2a = createLicense (Some NonOperatorLicense) (Round 2) + let Right mergedLicense1 = mergeValidatorLicenses [license1a, license2a] + mergedLicense1.kind === Some OperatorLicense + + -- Scenario 2: Operator + NonOperator → Operator + let license1b = createLicense (Some OperatorLicense) (Round 1) + let license2b = createLicense (Some NonOperatorLicense) (Round 2) + let Right mergedLicense2 = mergeValidatorLicenses [license1b, license2b] + mergedLicense2.kind === Some OperatorLicense + + -- Scenario 3: NonOperator + NonOperator → NonOperator + let license1c = createLicense (Some NonOperatorLicense) (Round 1) + let license2c = createLicense (Some NonOperatorLicense) (Round 2) + let Right mergedLicense3 = mergeValidatorLicenses [license1c, license2c] + mergedLicense3.kind === Some NonOperatorLicense + +-- Faucet state selection: license with highest lastReceivedFor is used as base +test_FaucetStateSelection_MaxLastReceivedFor : Script () +test_FaucetStateSelection_MaxLastReceivedFor = do + dso <- allocateParty "dso" + validator <- allocateParty "validator" + sponsor1 <- allocateParty "sponsor1" + sponsor2 <- allocateParty "sponsor2" + now <- getTime + + let license1 = ValidatorLicense with + validator + sponsor = sponsor1 + dso + faucetState = Some FaucetState with + firstReceivedFor = Round 0 + lastReceivedFor = Round 1 + numCouponsMissed = 0 + metadata = Some ValidatorLicenseMetadata with + version = "0.1.0" + contactPoint = "v1@example.com" + lastUpdatedAt = now + lastActiveAt = Some now + weight = None + kind = None + + let license2 = ValidatorLicense with + validator + sponsor = sponsor2 + dso + faucetState = Some FaucetState with + firstReceivedFor = Round 0 + lastReceivedFor = Round 5 -- Highest lastReceivedFor + numCouponsMissed = 2 + metadata = Some ValidatorLicenseMetadata with + version = "0.2.0" + contactPoint = "v2@example.com" + lastUpdatedAt = now + lastActiveAt = Some now + weight = None + kind = None + + let Right mergedLicense = mergeValidatorLicenses [license1, license2] + + -- Should use faucetState, sponsor and metadata from license2 (having highest lastReceivedFor) + mergedLicense.faucetState === license2.faucetState + mergedLicense.sponsor === license2.sponsor + mergedLicense.metadata === license2.metadata + +test_mergeValidation : Script () +test_mergeValidation = do + dso <- allocateParty "dso" + validator1 <- allocateParty "validator1" + validator2 <- allocateParty "validator2" + sponsor <- allocateParty "sponsor" + now <- getTime + + let license1 = ValidatorLicense with + validator = validator1 + sponsor + dso + faucetState = None + metadata = None + lastActiveAt = Some now + weight = None + kind = None + + let license2 = ValidatorLicense with + validator = validator2 + sponsor + dso + faucetState = None + metadata = None + lastActiveAt = Some now + weight = None + kind = None + + let result = mergeValidatorLicenses [license1] + result === Left "Number of validatorLicense contracts to merge is >= 2" + + let result = mergeValidatorLicenses [license1, license2] + result === Left "All validatorLicenses map to the same validator" diff --git a/daml/splice-dso-governance/daml.yaml b/daml/splice-dso-governance/daml.yaml index ee90977197..f8e314e1c9 100644 --- a/daml/splice-dso-governance/daml.yaml +++ b/daml/splice-dso-governance/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 name: splice-dso-governance source: daml -version: 0.1.20 +version: 0.1.21 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-dso-governance/daml/Splice/DsoRules.daml b/daml/splice-dso-governance/daml/Splice/DsoRules.daml index 71990a32ea..b4498a63f6 100644 --- a/daml/splice-dso-governance/daml/Splice/DsoRules.daml +++ b/daml/splice-dso-governance/daml/Splice/DsoRules.daml @@ -33,6 +33,7 @@ import Splice.SvOnboarding import Splice.DSO.AmuletPrice import Splice.DSO.DecentralizedSynchronizer import Splice.DSO.SvState +import Splice.DsoRules.Utils import Splice.Schedule import Splice.Util import Splice.CometBft() @@ -207,6 +208,9 @@ data DsoRules_RevokeFeaturedAppRightResult = DsoRules_RevokeFeaturedAppRightResu data DsoRules_OnboardValidatorResult = DsoRules_OnboardValidatorResult with validatorLicense : ContractId ValidatorLicense +data DsoRules_GrantValidatorLicenseResult = DsoRules_GrantValidatorLicenseResult with + validatorLicense : ContractId ValidatorLicense + data DsoRules_MergeValidatorLicenseResult = DsoRules_MergeValidatorLicenseResult with validatorLicense : ContractId ValidatorLicense @@ -979,28 +983,50 @@ template DsoRules with faucetState = None metadata = fmap (\version -> ValidatorLicenseMetadata with version; contactPoint = fromOptional "" contactPoint; lastUpdatedAt = now) version lastActiveAt = Some now + weight = None -- An individual SV can only create a license with the default weight. + kind = Some OperatorLicense return DsoRules_OnboardValidatorResult with .. + -- Grant ValidatorLicense to a party that is not an validator node operator + --------------------------------------------------------------------------- + + nonconsuming choice DsoRules_GrantValidatorLicense : DsoRules_GrantValidatorLicenseResult + with + sponsor : Party + validator : Party + controller sponsor + do + require "Sponsor is an SV" (sponsor `Map.member` svs) + now <- getTime + validatorLicense <- create ValidatorLicense with + dso + sponsor + validator + faucetState = None + metadata = None + lastActiveAt = Some now + weight = None + kind = Some NonOperatorLicense + return DsoRules_GrantValidatorLicenseResult with .. + nonconsuming choice DsoRules_MergeValidatorLicense : DsoRules_MergeValidatorLicenseResult - -- ^ Note: removes the old duplicated licenses and creates a new one with the highest lastReceivedFor round - -- There should never be duplicates going forward. + -- ^ Removes the old duplicated licenses and creates a new one with merged fields having value: + -- - lastReceivedFor: max value among all licenses + -- - weight: min value among all licenses + -- - kind: `Some OperatorLicense` if any license has `None` or `Some OperatorLicense` with validatorLicenseCids : [ContractId ValidatorLicense] sv : Optional Party controller sv do _ <- getAndValidateSvParty this sv - require "Number of validatorLicense contracts to merge is >= 2" (length validatorLicenseCids >= 2) validatorLicenses <- forA validatorLicenseCids $ fetchAndArchive (ForDso this.dso) - require "All validatorLicenses map to the same validator" ( length (dedup (map (.validator) validatorLicenses)) == 1) - -- We don't attempt to merge the fields in the validator licence since there is no good - -- way to do so and none of the fields other than lastReceivedFor round are crucial and for that - -- taking the max is the correct merge. - cid <- create $ maximumOn (\license -> - case license.faucetState of - None -> Round 0 - Some r -> r.lastReceivedFor) validatorLicenses - pure $ DsoRules_MergeValidatorLicenseResult cid + mergedLicense <- case mergeValidatorLicenses validatorLicenses of + Left err -> abort err + Right license -> pure license + + cid <- create mergedLicense + pure $ DsoRules_MergeValidatorLicenseResult cid -- SV onboarding ---------------- @@ -1251,8 +1277,6 @@ template DsoRules with sv : Optional Party controller sv do - -- TODO(#2025) Deprecate AmuletRules_ClaimExpiredRewards. We did not do that in the same change - -- as inlining the definition here so don't need to bump splice-amulet which also requires validators to upgrade. let AmuletRules_ClaimExpiredRewards{..} = choiceArg -- retrieve round closedRound <- fetchReferenceData (ForDso with dso) closedRoundCid @@ -1271,10 +1295,15 @@ template DsoRules with let numValidatorFaucetCoupons = optional 0.0 (intToDecimal . length) optValidatorFaucetCouponCids -- expire validator liveness activity records and compute their unclaimed rewards - forA_ (fromOptional [] optValidatorLivenessActivityRecordCids) $ \validatorLivenessActivityRecordCid -> do - exercise validatorLivenessActivityRecordCid ValidatorLivenessActivityRecord_DsoExpire with closedRoundCid + expiredValidatorLivenessActivityRecords <- forA (fromOptional [] optValidatorLivenessActivityRecordCids) $ \validatorLivenessActivityRecordCid -> do + -- We need to fetch the weight, and would also prefer doing 'Expire' + -- We can't do checked fetch as the owner is the validator + -- hence can't use either fetchAndArchive or fetchUncheckedAndArchive + record <- fetchUncheckedButArchiveLater validatorLivenessActivityRecordCid + exercise validatorLivenessActivityRecordCid ValidatorLivenessActivityRecord_DsoExpire with closedRoundCid + return (fromOptional defaultValidatorLicenseWeight record.weight) - let numValidatorLivenessActivityRecords = optional 0.0 (intToDecimal . length) optValidatorLivenessActivityRecordCids + let validatorLivenessActivityRecordsWeight = sum expiredValidatorLivenessActivityRecords -- expire app rewards and compute their unclaimed rewards expiredAppRewards <- forA appCouponCids $ \appCouponCid -> do @@ -1294,7 +1323,7 @@ template DsoRules with -- create unclaimed reward for the total let amount = validatorRewards * closedRound.issuancePerValidatorRewardCoupon + - (numValidatorFaucetCoupons + numValidatorLivenessActivityRecords) * getClosedMiningRoundIssuancePerValidatorFaucetCoupon closedRound + + (numValidatorFaucetCoupons + validatorLivenessActivityRecordsWeight) * getClosedMiningRoundIssuancePerValidatorFaucetCoupon closedRound + featuredAppRewards * closedRound.issuancePerFeaturedAppRewardCoupon + unfeaturedAppRewards * closedRound.issuancePerUnfeaturedAppRewardCoupon + intToDecimal svRewardsWeight * closedRound.issuancePerSvRewardCoupon diff --git a/daml/splice-dso-governance/daml/Splice/DsoRules/Utils.daml b/daml/splice-dso-governance/daml/Splice/DsoRules/Utils.daml new file mode 100644 index 0000000000..cb741b91a0 --- /dev/null +++ b/daml/splice-dso-governance/daml/Splice/DsoRules/Utils.daml @@ -0,0 +1,55 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +module Splice.DsoRules.Utils where + +import DA.List (dedup, maximumOn) +import qualified DA.List.Total +import DA.Optional (catOptionals) + +import Splice.ValidatorLicense +import Splice.Types (Round(..)) + +-- | Merges a list of `ValidatorLicense` contracts into a single license. +-- The merged license will have the following properties: +-- - faucetState: Uses the `faucetState` from the license with the highest `lastReceivedFor` round +-- - weight: Use the minimum weight among all licenses, `None` if all weights are `None`. +-- Since weight with `Some` values can only happen via SV vote, they are +-- preferred over `None`. We don't want any single SV to overwrite the +-- weight of a `ValidatorLicense` by simply granting a new one with weight +-- `None`. Therefore we select the minimum value among the `Some` values +-- even if that happens to be higher than the default value of 1.0 of `None` +-- - kind: `OperatorLicense` if any license has `None` or `Some OperatorLicense`, otherwise `NonOperatorLicense` +mergeValidatorLicenses : [ValidatorLicense] -> Either Text ValidatorLicense +mergeValidatorLicenses licenses = do + if length licenses < 2 + then Left "Number of validatorLicense contracts to merge is >= 2" + else pure () + + let validators = dedup (map (.validator) licenses) + if length validators /= 1 + then Left "All validatorLicenses map to the same validator" + else pure () + + -- Select base license: the one with max lastReceivedFor in faucetState + let baseLicense = maximumOn (\license -> + case license.faucetState of + None -> Round 0 + Some fs -> fs.lastReceivedFor) licenses + + -- Calculate merged weight: minimum of all non-None weights, or None if all are None + let nonNoneWeights = catOptionals $ map (.weight) licenses + let mergedWeight = DA.List.Total.minimumOn identity nonNoneWeights + + -- Calculate merged kind: OperatorLicense if any license has None or Some OperatorLicense + let hasOperatorLicense kind = case kind of + None -> True + Some OperatorLicense -> True + Some _ -> False + let mergedKind = if any hasOperatorLicense (map (.kind) licenses) + then Some OperatorLicense + else Some NonOperatorLicense + + pure $ baseLicense with + weight = mergedWeight + kind = mergedKind diff --git a/daml/splice-util-featured-app-proxies-test/daml.yaml b/daml/splice-util-featured-app-proxies-test/daml.yaml index a473f17f27..09aa3a084c 100644 --- a/daml/splice-util-featured-app-proxies-test/daml.yaml +++ b/daml/splice-util-featured-app-proxies-test/daml.yaml @@ -11,7 +11,7 @@ description: | are normal .dar files and can be shared by copying the .dars. (TODO(#594): remove this limitation) source: daml -version: 1.0.4 +version: 1.0.5 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-util-token-standard-wallet-test/daml.yaml b/daml/splice-util-token-standard-wallet-test/daml.yaml index 888b5ef560..f48fa48e42 100644 --- a/daml/splice-util-token-standard-wallet-test/daml.yaml +++ b/daml/splice-util-token-standard-wallet-test/daml.yaml @@ -4,7 +4,7 @@ sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 name: splice-util-token-standard-wallet-test source: daml -version: 1.0.0 +version: 1.0.1 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-wallet-payments/daml.yaml b/daml/splice-wallet-payments/daml.yaml index ba6e3a65b0..3c101ac4a2 100644 --- a/daml/splice-wallet-payments/daml.yaml +++ b/daml/splice-wallet-payments/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 name: splice-wallet-payments source: daml -version: 0.1.14 +version: 0.1.15 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-wallet-test/daml.yaml b/daml/splice-wallet-test/daml.yaml index 8a2224d54e..4426c9813a 100644 --- a/daml/splice-wallet-test/daml.yaml +++ b/daml/splice-wallet-test/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 name: splice-wallet-test source: daml -version: 0.1.16 +version: 0.1.17 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-wallet/daml.yaml b/daml/splice-wallet/daml.yaml index 77310ac0a6..cf1971b83f 100644 --- a/daml/splice-wallet/daml.yaml +++ b/daml/splice-wallet/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 name: splice-wallet source: daml -version: 0.1.14 +version: 0.1.15 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splitwell-test/daml.yaml b/daml/splitwell-test/daml.yaml index 16969d9d23..dde8ced5ea 100644 --- a/daml/splitwell-test/daml.yaml +++ b/daml/splitwell-test/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 name: splitwell-test source: daml -version: 0.1.16 +version: 0.1.17 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splitwell/daml.yaml b/daml/splitwell/daml.yaml index e1e7d4f729..762e112666 100644 --- a/daml/splitwell/daml.yaml +++ b/daml/splitwell/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 name: splitwell source: daml -version: 0.1.14 +version: 0.1.15 dependencies: - daml-prim - daml-stdlib diff --git a/token-standard/splice-token-standard-test/daml.yaml b/token-standard/splice-token-standard-test/daml.yaml index 3c407dd965..2cc5e6b52a 100644 --- a/token-standard/splice-token-standard-test/daml.yaml +++ b/token-standard/splice-token-standard-test/daml.yaml @@ -16,7 +16,7 @@ description: | as Daml script code can currently not be shared via .dars across SDKs. The dependencies are normal .dar files and can be shared by copying the .dars. (TODO(#594): remove this limitation) -version: 1.0.7 +version: 1.0.8 source: daml dependencies: - daml-prim