diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AppUpgradeIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AppUpgradeIntegrationTest.scala index ef7de46f4c..e857d1d03d 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AppUpgradeIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/AppUpgradeIntegrationTest.scala @@ -265,6 +265,7 @@ class AppUpgradeIntegrationTest amuletConfig.featuredAppActivityMarkerAmount, amuletConfig.optDevelopmentFundManager, amuletConfig.externalPartyConfigStateTickDuration, + amuletConfig.rewardConfig, ) val upgradeAction = new ARC_AmuletRules( new CRARC_SetConfig( 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 9d8b73cc7a..dc32263e18 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 @@ -233,6 +233,7 @@ class BootstrapPackageConfigIntegrationTest amuletConfig.featuredAppActivityMarkerAmount, amuletConfig.optDevelopmentFundManager, amuletConfig.externalPartyConfigStateTickDuration, + amuletConfig.rewardConfig, ) val upgradeAction = new ARC_AmuletRules( @@ -382,6 +383,7 @@ class BootstrapPackageConfigIntegrationTest amuletConfig.featuredAppActivityMarkerAmount, amuletConfig.optDevelopmentFundManager, amuletConfig.externalPartyConfigStateTickDuration, + amuletConfig.rewardConfig, ) val upgradeAction = new ARC_AmuletRules( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundFrontendTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundFrontendTimeBasedIntegrationTest.scala index dae2872816..d2fab3a049 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundFrontendTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/DevelopmentFundFrontendTimeBasedIntegrationTest.scala @@ -444,6 +444,7 @@ class DevelopmentFundFrontendTimeBasedIntegrationTest existingConfig.featuredAppActivityMarkerAmount, Optional.of(newDfm.toProtoPrimitive), existingConfig.externalPartyConfigStateTickDuration, + existingConfig.rewardConfig, ) val action = new ARC_AmuletRules( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvReconcileSynchronizerConfigIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvReconcileSynchronizerConfigIntegrationTest.scala index 37181e5699..b5c9bca0a1 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvReconcileSynchronizerConfigIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvReconcileSynchronizerConfigIntegrationTest.scala @@ -143,6 +143,7 @@ class SvReconcileSynchronizerConfigIntegrationTest extends SvIntegrationTestBase amuletConfig.featuredAppActivityMarkerAmount, amuletConfig.optDevelopmentFundManager, amuletConfig.externalPartyConfigStateTickDuration, + amuletConfig.rewardConfig, ) } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvStateManagementIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvStateManagementIntegrationTest.scala index 32315a2050..e4d2e3fe12 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvStateManagementIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvStateManagementIntegrationTest.scala @@ -495,6 +495,7 @@ class SvStateManagementIntegrationTest extends SvIntegrationTestBase with Trigge initialConfig.featuredAppActivityMarkerAmount, initialConfig.optDevelopmentFundManager, initialConfig.externalPartyConfigStateTickDuration, + initialConfig.rewardConfig, ) val (_, voteRequestCid) = actAndCheck( diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnsupportedPackageVettingIntegrationTest.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnsupportedPackageVettingIntegrationTest.scala index d0bac5a867..ad578c379a 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnsupportedPackageVettingIntegrationTest.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/UnsupportedPackageVettingIntegrationTest.scala @@ -166,6 +166,7 @@ class UnsupportedPackageVettingIntegrationTest currentConfig.featuredAppActivityMarkerAmount, currentConfig.optDevelopmentFundManager, currentConfig.externalPartyConfigStateTickDuration, + currentConfig.rewardConfig, ) setAmuletConfig(Seq((None, newAmuletConfig, currentConfig))) } diff --git a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/AmuletConfigUtil.scala b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/AmuletConfigUtil.scala index afa70c6662..9634c73da2 100644 --- a/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/AmuletConfigUtil.scala +++ b/apps/app/src/test/scala/org/lfdecentralizedtrust/splice/util/AmuletConfigUtil.scala @@ -63,6 +63,7 @@ trait AmuletConfigUtil extends TestCommon { existingAmuletConfig.featuredAppActivityMarkerAmount, existingAmuletConfig.optDevelopmentFundManager, existingAmuletConfig.externalPartyConfigStateTickDuration, + existingAmuletConfig.rewardConfig, ) } diff --git a/apps/common/frontend-test-handlers/src/mocks/helpers/amulet-config-helper.ts b/apps/common/frontend-test-handlers/src/mocks/helpers/amulet-config-helper.ts index ac94e2256b..92f8a341d6 100644 --- a/apps/common/frontend-test-handlers/src/mocks/helpers/amulet-config-helper.ts +++ b/apps/common/frontend-test-handlers/src/mocks/helpers/amulet-config-helper.ts @@ -227,6 +227,13 @@ export function getAmuletRulesConfig( featuredAppActivityMarkerAmount: null, optDevelopmentFundManager: null, externalPartyConfigStateTickDuration: null, + rewardConfig: { + mintingVersion: 'RewardVersion_FeaturedAppMarkers', + dryRunVersion: null, + batchSize: '100', + rewardCouponTimeToLive: { microseconds: '129600000000' }, + appRewardCouponThreshold: '0.5', + }, }; } @@ -394,6 +401,17 @@ export function getExpectedAmuletRulesConfigDiffsHTML( "validatorLifecycle": "0.1.2", "wallet": "0.1.8", "walletPayments": "0.1.8" +}
  • rewardConfig
    {
    +  "mintingVersion": "RewardVersion_FeaturedAppMarkers",
    +  "dryRunVersion": null,
    +  "batchSize": "100",
    +  "rewardCouponTimeToLive": {
    +    "microseconds": "129600000000"
    +  },
    +  "appRewardCouponThreshold": "0.5"
     }
  • tickDuration
    new RelTime(TimeUnit.NANOSECONDS.toMicros(t.duration.toNanos))) .toJava, + Optional.empty(), // rewardConfig ) def defaultAnsConfig( diff --git a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/StoreTestBase.scala b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/StoreTestBase.scala index a7f4a27b1a..090557663c 100644 --- a/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/StoreTestBase.scala +++ b/apps/common/src/test/scala/org/lfdecentralizedtrust/splice/store/StoreTestBase.scala @@ -282,6 +282,8 @@ abstract class StoreTestBase SpliceUtil.defaultTransferConfig(10, holdingFee), SpliceUtil.issuanceConfig(10.0, 10.0, 10.0), new RelTime(1_000_000), + Optional.empty(), // trafficPrice + Optional.empty(), // rewardConfig ) contract( diff --git a/apps/dar-resources-generator/src/main/scala/org/lfdecentralizedtrust/splice/darutils/DarResources.scala b/apps/dar-resources-generator/src/main/scala/org/lfdecentralizedtrust/splice/darutils/DarResources.scala index a489f51e5d..cd36e92add 100644 --- a/apps/dar-resources-generator/src/main/scala/org/lfdecentralizedtrust/splice/darutils/DarResources.scala +++ b/apps/dar-resources-generator/src/main/scala/org/lfdecentralizedtrust/splice/darutils/DarResources.scala @@ -82,6 +82,7 @@ object DarResources { lazy val amulet_0_1_15 = DarResource("splice-amulet-0.1.15.dar") lazy val amulet_0_1_16 = DarResource("splice-amulet-0.1.16.dar") lazy val amulet_0_1_17 = DarResource("splice-amulet-0.1.17.dar") + lazy val amulet_0_1_18 = DarResource("splice-amulet-0.1.18.dar") lazy val amulet_current = DarResource("splice-amulet-current.dar") lazy val amulet = PackageResource( amulet_current, @@ -105,6 +106,7 @@ object DarResources { amulet_0_1_15, amulet_0_1_16, amulet_0_1_17, + amulet_0_1_18, ), ) @@ -133,6 +135,7 @@ object DarResources { lazy val dsoGovernance_0_1_22 = DarResource("splice-dso-governance-0.1.22.dar") lazy val dsoGovernance_0_1_23 = DarResource("splice-dso-governance-0.1.23.dar") lazy val dsoGovernance_0_1_24 = DarResource("splice-dso-governance-0.1.24.dar") + lazy val dsoGovernance_0_1_25 = DarResource("splice-dso-governance-0.1.25.dar") lazy val dsoGovernance_current = DarResource("splice-dso-governance-current.dar") lazy val dsoGovernance = PackageResource( dsoGovernance_current, @@ -163,6 +166,7 @@ object DarResources { dsoGovernance_0_1_22, dsoGovernance_0_1_23, dsoGovernance_0_1_24, + dsoGovernance_0_1_25, ), ) @@ -185,6 +189,7 @@ object DarResources { lazy val amuletNameService_0_1_16 = DarResource("splice-amulet-name-service-0.1.16.dar") lazy val amuletNameService_0_1_17 = DarResource("splice-amulet-name-service-0.1.17.dar") lazy val amuletNameService_0_1_18 = DarResource("splice-amulet-name-service-0.1.18.dar") + lazy val amuletNameService_0_1_19 = DarResource("splice-amulet-name-service-0.1.19.dar") lazy val amuletNameService_current = DarResource("splice-amulet-name-service-current.dar") lazy val amuletNameService = PackageResource( amuletNameService_current, @@ -209,6 +214,7 @@ object DarResources { amuletNameService_0_1_16, amuletNameService_0_1_17, amuletNameService_0_1_18, + amuletNameService_0_1_19, ), ) @@ -231,6 +237,7 @@ object DarResources { lazy val splitwell_0_1_16 = DarResource("splitwell-0.1.16.dar") lazy val splitwell_0_1_17 = DarResource("splitwell-0.1.17.dar") lazy val splitwell_0_1_18 = DarResource("splitwell-0.1.18.dar") + lazy val splitwell_0_1_19 = DarResource("splitwell-0.1.19.dar") lazy val splitwell_current = DarResource("splitwell-current.dar") lazy val splitwell = PackageResource( splitwell_current, @@ -255,6 +262,7 @@ object DarResources { splitwell_0_1_16, splitwell_0_1_17, splitwell_0_1_18, + splitwell_0_1_19, ), ) @@ -277,6 +285,7 @@ object DarResources { lazy val wallet_0_1_16 = DarResource("splice-wallet-0.1.16.dar") lazy val wallet_0_1_17 = DarResource("splice-wallet-0.1.17.dar") lazy val wallet_0_1_18 = DarResource("splice-wallet-0.1.18.dar") + lazy val wallet_0_1_19 = DarResource("splice-wallet-0.1.19.dar") lazy val wallet_current = DarResource("splice-wallet-current.dar") lazy val wallet = PackageResource( wallet_current, @@ -301,6 +310,7 @@ object DarResources { wallet_0_1_16, wallet_0_1_17, wallet_0_1_18, + wallet_0_1_19, ), ) @@ -322,6 +332,7 @@ object DarResources { lazy val walletPayments_0_1_15 = DarResource("splice-wallet-payments-0.1.15.dar") lazy val walletPayments_0_1_16 = DarResource("splice-wallet-payments-0.1.16.dar") lazy val walletPayments_0_1_17 = DarResource("splice-wallet-payments-0.1.17.dar") + lazy val walletPayments_0_1_18 = DarResource("splice-wallet-payments-0.1.18.dar") lazy val walletPayments_current = DarResource("splice-wallet-payments-current.dar") lazy val walletPayments = PackageResource( walletPayments_current, @@ -345,6 +356,7 @@ object DarResources { walletPayments_0_1_15, walletPayments_0_1_16, walletPayments_0_1_17, + walletPayments_0_1_18, ), ) diff --git a/apps/package-lock.json b/apps/package-lock.json index dec5ff93b8..ea9edf1544 100644 --- a/apps/package-lock.json +++ b/apps/package-lock.json @@ -26,13 +26,13 @@ "wallet/external-openapi-ts-client" ], "dependencies": { - "@daml.js/ans": "file:common/frontend/daml.js/splice-amulet-name-service-0.1.18", - "@daml.js/splice-amulet": "file:common/frontend/daml.js/splice-amulet-0.1.17", - "@daml.js/splice-dso-governance": "file:common/frontend/daml.js/splice-dso-governance-0.1.24", + "@daml.js/ans": "file:common/frontend/daml.js/splice-amulet-name-service-0.1.19", + "@daml.js/splice-amulet": "file:common/frontend/daml.js/splice-amulet-0.1.18", + "@daml.js/splice-dso-governance": "file:common/frontend/daml.js/splice-dso-governance-0.1.25", "@daml.js/splice-validator-lifecycle": "file:common/frontend/daml.js/splice-validator-lifecycle-0.1.6", - "@daml.js/splice-wallet": "file:common/frontend/daml.js/splice-wallet-0.1.18", - "@daml.js/splice-wallet-payments": "file:common/frontend/daml.js/splice-wallet-payments-0.1.17", - "@daml.js/splitwell": "file:common/frontend/daml.js/splitwell-0.1.18", + "@daml.js/splice-wallet": "file:common/frontend/daml.js/splice-wallet-0.1.19", + "@daml.js/splice-wallet-payments": "file:common/frontend/daml.js/splice-wallet-payments-0.1.18", + "@daml.js/splitwell": "file:common/frontend/daml.js/splitwell-0.1.19", "xunit-viewer": "^10.6.1" } }, @@ -497,6 +497,26 @@ "common/frontend/daml.js/splice-amulet-0.1.17": { "name": "@daml.js/splice-amulet-0.1.17", "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-featured-app-v2-1.0.0": "file:../splice-api-featured-app-v2-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.18": { + "name": "@daml.js/splice-amulet-0.1.18", + "version": "0.0.0", "license": "UNLICENSED", "dependencies": { "@daml.js/daml-prim-DA-Types-1.0.0": "file:../daml-prim-DA-Types-1.0.0", @@ -642,6 +662,7 @@ "common/frontend/daml.js/splice-amulet-name-service-0.1.18": { "name": "@daml.js/splice-amulet-name-service-0.1.18", "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", @@ -652,6 +673,19 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splice-amulet-name-service-0.1.19": { + "name": "@daml.js/splice-amulet-name-service-0.1.19", + "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.18": "file:../splice-amulet-0.1.18", + "@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.18": "file:../splice-wallet-payments-0.1.18", + "@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", @@ -848,6 +882,7 @@ "common/frontend/daml.js/splice-dso-governance-0.1.24": { "name": "@daml.js/splice-dso-governance-0.1.24", "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", @@ -860,6 +895,21 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splice-dso-governance-0.1.25": { + "name": "@daml.js/splice-dso-governance-0.1.25", + "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.18": "file:../splice-amulet-0.1.18", + "@daml.js/splice-amulet-name-service-0.1.19": "file:../splice-amulet-name-service-0.1.19", + "@daml.js/splice-wallet-payments-0.1.18": "file:../splice-wallet-payments-0.1.18", + "@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", @@ -1038,6 +1088,7 @@ "common/frontend/daml.js/splice-wallet-0.1.18": { "name": "@daml.js/splice-wallet-0.1.18", "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", @@ -1051,6 +1102,22 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splice-wallet-0.1.19": { + "name": "@daml.js/splice-wallet-0.1.19", + "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.18": "file:../splice-amulet-0.1.18", + "@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.18": "file:../splice-wallet-payments-0.1.18", + "@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", @@ -1162,6 +1229,7 @@ "common/frontend/daml.js/splice-wallet-payments-0.1.17": { "name": "@daml.js/splice-wallet-payments-0.1.17", "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", @@ -1171,6 +1239,18 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splice-wallet-payments-0.1.18": { + "name": "@daml.js/splice-wallet-payments-0.1.18", + "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.18": "file:../splice-amulet-0.1.18", + "@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", @@ -1299,6 +1379,7 @@ "common/frontend/daml.js/splitwell-0.1.18": { "name": "@daml.js/splitwell-0.1.18", "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", @@ -1309,6 +1390,19 @@ "@mojotech/json-type-validation": "^3.1.0" } }, + "common/frontend/daml.js/splitwell-0.1.19": { + "name": "@daml.js/splitwell-0.1.19", + "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.18": "file:../splice-amulet-0.1.18", + "@daml.js/splice-wallet-payments-0.1.18": "file:../splice-wallet-payments-0.1.18", + "@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", @@ -1712,7 +1806,7 @@ } }, "node_modules/@daml.js/ans": { - "resolved": "common/frontend/daml.js/splice-amulet-name-service-0.1.18", + "resolved": "common/frontend/daml.js/splice-amulet-name-service-0.1.19", "link": true }, "node_modules/@daml.js/daml-prim-DA-Types-1.0.0": { @@ -1732,15 +1826,15 @@ "link": true }, "node_modules/@daml.js/splice-amulet": { - "resolved": "common/frontend/daml.js/splice-amulet-0.1.17", + "resolved": "common/frontend/daml.js/splice-amulet-0.1.18", "link": true }, - "node_modules/@daml.js/splice-amulet-0.1.17": { - "resolved": "common/frontend/daml.js/splice-amulet-0.1.17", + "node_modules/@daml.js/splice-amulet-0.1.18": { + "resolved": "common/frontend/daml.js/splice-amulet-0.1.18", "link": true }, - "node_modules/@daml.js/splice-amulet-name-service-0.1.18": { - "resolved": "common/frontend/daml.js/splice-amulet-name-service-0.1.18", + "node_modules/@daml.js/splice-amulet-name-service-0.1.19": { + "resolved": "common/frontend/daml.js/splice-amulet-name-service-0.1.19", "link": true }, "node_modules/@daml.js/splice-api-featured-app-v1-1.0.0": { @@ -1784,7 +1878,7 @@ "link": true }, "node_modules/@daml.js/splice-dso-governance": { - "resolved": "common/frontend/daml.js/splice-dso-governance-0.1.24", + "resolved": "common/frontend/daml.js/splice-dso-governance-0.1.25", "link": true }, "node_modules/@daml.js/splice-validator-lifecycle": { @@ -1792,19 +1886,19 @@ "link": true }, "node_modules/@daml.js/splice-wallet": { - "resolved": "common/frontend/daml.js/splice-wallet-0.1.18", + "resolved": "common/frontend/daml.js/splice-wallet-0.1.19", "link": true }, "node_modules/@daml.js/splice-wallet-payments": { - "resolved": "common/frontend/daml.js/splice-wallet-payments-0.1.17", + "resolved": "common/frontend/daml.js/splice-wallet-payments-0.1.18", "link": true }, - "node_modules/@daml.js/splice-wallet-payments-0.1.17": { - "resolved": "common/frontend/daml.js/splice-wallet-payments-0.1.17", + "node_modules/@daml.js/splice-wallet-payments-0.1.18": { + "resolved": "common/frontend/daml.js/splice-wallet-payments-0.1.18", "link": true }, "node_modules/@daml.js/splitwell": { - "resolved": "common/frontend/daml.js/splitwell-0.1.18", + "resolved": "common/frontend/daml.js/splitwell-0.1.19", "link": true }, "node_modules/@daml/ledger": { diff --git a/apps/package.json b/apps/package.json index d407af84cc..645587499b 100644 --- a/apps/package.json +++ b/apps/package.json @@ -21,13 +21,13 @@ "wallet/external-openapi-ts-client" ], "dependencies": { - "@daml.js/ans": "file:common/frontend/daml.js/splice-amulet-name-service-0.1.18", - "@daml.js/splice-amulet": "file:common/frontend/daml.js/splice-amulet-0.1.17", - "@daml.js/splice-dso-governance": "file:common/frontend/daml.js/splice-dso-governance-0.1.24", + "@daml.js/ans": "file:common/frontend/daml.js/splice-amulet-name-service-0.1.19", + "@daml.js/splice-amulet": "file:common/frontend/daml.js/splice-amulet-0.1.18", + "@daml.js/splice-dso-governance": "file:common/frontend/daml.js/splice-dso-governance-0.1.25", "@daml.js/splice-validator-lifecycle": "file:common/frontend/daml.js/splice-validator-lifecycle-0.1.6", - "@daml.js/splice-wallet": "file:common/frontend/daml.js/splice-wallet-0.1.18", - "@daml.js/splice-wallet-payments": "file:common/frontend/daml.js/splice-wallet-payments-0.1.17", - "@daml.js/splitwell": "file:common/frontend/daml.js/splitwell-0.1.18", + "@daml.js/splice-wallet": "file:common/frontend/daml.js/splice-wallet-0.1.19", + "@daml.js/splice-wallet-payments": "file:common/frontend/daml.js/splice-wallet-payments-0.1.18", + "@daml.js/splitwell": "file:common/frontend/daml.js/splitwell-0.1.19", "xunit-viewer": "^10.6.1" } } diff --git a/apps/scan/frontend/src/__tests__/mocks/data.ts b/apps/scan/frontend/src/__tests__/mocks/data.ts index 56b142f7cc..d11cd445e1 100644 --- a/apps/scan/frontend/src/__tests__/mocks/data.ts +++ b/apps/scan/frontend/src/__tests__/mocks/data.ts @@ -177,6 +177,7 @@ export function amuletRules(zeroTransferFees: boolean): any { featuredAppActivityMarkerAmount: null, optDevelopmentFundManager: null, externalPartyConfigStateTickDuration: null, + rewardConfig: null, }, futureValues: [], }, diff --git a/apps/sv/frontend/src/__tests__/utils/buildAmuletRulesConfigFromChanges.test.ts b/apps/sv/frontend/src/__tests__/utils/buildAmuletRulesConfigFromChanges.test.ts index b03af7d36a..a0bea6e3d9 100644 --- a/apps/sv/frontend/src/__tests__/utils/buildAmuletRulesConfigFromChanges.test.ts +++ b/apps/sv/frontend/src/__tests__/utils/buildAmuletRulesConfigFromChanges.test.ts @@ -218,6 +218,36 @@ describe('buildAmuletRulesConfigFromChanges', () => { currentValue: '0.1.1', newValue: '0.2.0', }, + { + fieldName: 'rewardConfigMintingVersion', + label: 'Reward config: Minting version', + currentValue: 'RewardVersion_FeaturedAppMarkers', + newValue: 'RewardVersion_TrafficBasedAppRewards', + }, + { + fieldName: 'rewardConfigDryRunVersion', + label: 'Reward config: Dry-run version', + currentValue: '', + newValue: 'RewardVersion_TrafficBasedAppRewards', + }, + { + fieldName: 'rewardConfigBatchSize', + label: 'Reward config: Batch size', + currentValue: '100', + newValue: '200', + }, + { + fieldName: 'rewardConfigRewardCouponTimeToLive', + label: 'Reward config: Reward coupon time to live (microseconds)', + currentValue: '129600000000', + newValue: '259200000000', + }, + { + fieldName: 'rewardConfigAppRewardCouponThreshold', + label: 'Reward config: App reward coupon threshold ($)', + currentValue: '0.5', + newValue: '1.0', + }, ]; const result = buildAmuletRulesConfigFromChanges(changes); @@ -264,6 +294,14 @@ describe('buildAmuletRulesConfigFromChanges', () => { expect(result.packageConfig.validatorLifecycle).toBe('0.2.0'); expect(result.packageConfig.wallet).toBe('0.2.0'); expect(result.packageConfig.walletPayments).toBe('0.2.0'); + + expect(result.rewardConfig).toEqual({ + mintingVersion: 'RewardVersion_TrafficBasedAppRewards', + dryRunVersion: 'RewardVersion_TrafficBasedAppRewards', + batchSize: '200', + rewardCouponTimeToLive: { microseconds: '259200000000' }, + appRewardCouponThreshold: '1.0', + }); }); test('should handle multiple transfer fee steps', () => { diff --git a/apps/sv/frontend/src/utils/buildAmuletConfigChanges.ts b/apps/sv/frontend/src/utils/buildAmuletConfigChanges.ts index 5597780364..ac14e26031 100644 --- a/apps/sv/frontend/src/utils/buildAmuletConfigChanges.ts +++ b/apps/sv/frontend/src/utils/buildAmuletConfigChanges.ts @@ -1,7 +1,11 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 import { Optional } from '@daml/types'; -import { AmuletConfig, PackageConfig } from '@daml.js/splice-amulet/lib/Splice/AmuletConfig'; +import { + AmuletConfig, + PackageConfig, + RewardConfig, +} from '@daml.js/splice-amulet/lib/Splice/AmuletConfig'; import { Tuple2 } from '@daml.js/daml-prim-DA-Types-1.0.0/lib/DA/Types'; import { Set as DamlSet } from '@daml.js/daml-stdlib-DA-Set-Types-1.0.0/lib/DA/Set/Types'; import { RelTime } from '@daml.js/daml-stdlib-DA-Time-Types-1.0.0/lib/DA/Time/Types'; @@ -105,6 +109,8 @@ export function buildAmuletConfigChanges( ), ...buildPackageConfigChanges(before?.packageConfig, after?.packageConfig), + + ...buildRewardConfigChanges(before?.rewardConfig, after?.rewardConfig), ] as ConfigChange[]; return showAllFields ? changes : changes.filter(c => c.currentValue !== c.newValue); @@ -308,6 +314,44 @@ function buildIssuanceCurveChanges( return [...initialValues, ...futureValues]; } +function buildRewardConfigChanges( + before: RewardConfig | null | undefined, + after: RewardConfig | null | undefined +) { + return [ + { + fieldName: 'rewardConfigMintingVersion', + label: 'Reward config: Minting version', + currentValue: before?.mintingVersion || '', + newValue: after?.mintingVersion || '', + }, + { + fieldName: 'rewardConfigDryRunVersion', + label: 'Reward config: Dry-run version', + currentValue: before?.dryRunVersion || '', + newValue: after?.dryRunVersion || '', + }, + { + fieldName: 'rewardConfigBatchSize', + label: 'Reward config: Batch size', + currentValue: before?.batchSize || '', + newValue: after?.batchSize || '', + }, + { + fieldName: 'rewardConfigRewardCouponTimeToLive', + label: 'Reward config: Reward coupon time to live (microseconds)', + currentValue: before?.rewardCouponTimeToLive.microseconds || '', + newValue: after?.rewardCouponTimeToLive.microseconds || '', + }, + { + fieldName: 'rewardConfigAppRewardCouponThreshold', + label: 'Reward config: App reward coupon threshold ($)', + currentValue: before?.appRewardCouponThreshold || '', + newValue: after?.appRewardCouponThreshold || '', + }, + ] as ConfigChange[]; +} + function buildDecentralizedSynchronizerChanges( before: AmuletDecentralizedSynchronizerConfig | undefined, after: AmuletDecentralizedSynchronizerConfig | undefined diff --git a/apps/sv/frontend/src/utils/buildAmuletRulesConfigFromChanges.ts b/apps/sv/frontend/src/utils/buildAmuletRulesConfigFromChanges.ts index ce9a8ab1d6..ab43f8a833 100644 --- a/apps/sv/frontend/src/utils/buildAmuletRulesConfigFromChanges.ts +++ b/apps/sv/frontend/src/utils/buildAmuletRulesConfigFromChanges.ts @@ -1,7 +1,7 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { AmuletConfig } from '@daml.js/splice-amulet/lib/Splice/AmuletConfig'; +import { AmuletConfig, RewardVersion } from '@daml.js/splice-amulet/lib/Splice/AmuletConfig'; import { Tuple2 } from '@daml.js/daml-prim-DA-Types-1.0.0/lib/DA/Types'; import * as damlTypes from '@daml/types'; import { RelTime } from '@daml.js/daml-stdlib-DA-Time-Types-1.0.0/lib/DA/Time/Types'; @@ -85,6 +85,7 @@ export function buildAmuletRulesConfigFromChanges( 'issuanceCurveInitialValueOptDevelopmentFundPercentage' ); const externalPartyConfigStateTickDuration = getValue('externalPartyConfigStateTickDuration'); + const rewardConfigMintingVersion = getValue('rewardConfigMintingVersion'); const amuletConfig: AmuletConfig<'USD'> = { tickDuration: { microseconds: getValue('tickDuration') }, transferPreapprovalFee: transferPreapprovalFee === '' ? null : transferPreapprovalFee, @@ -147,6 +148,22 @@ export function buildAmuletRulesConfigFromChanges( wallet: getValue('packageConfigWallet'), walletPayments: getValue('packageConfigWalletPayments'), }, + + rewardConfig: + rewardConfigMintingVersion === '' + ? null + : { + mintingVersion: getValue('rewardConfigMintingVersion') as RewardVersion, + dryRunVersion: + getValue('rewardConfigDryRunVersion') === '' + ? null + : (getValue('rewardConfigDryRunVersion') as RewardVersion), + batchSize: getValue('rewardConfigBatchSize'), + rewardCouponTimeToLive: { + microseconds: getValue('rewardConfigRewardCouponTimeToLive'), + }, + appRewardCouponThreshold: getValue('rewardConfigAppRewardCouponThreshold'), + }, }; return amuletConfig; diff --git a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvDsoStore.scala b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvDsoStore.scala index 1b932b4bd2..5fed332d0c 100644 --- a/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvDsoStore.scala +++ b/apps/sv/src/main/scala/org/lfdecentralizedtrust/splice/sv/store/SvDsoStore.scala @@ -1265,6 +1265,35 @@ object SvDsoStore { rewardWeight = Some(contract.payload.weight), ) }, + mkFilter(splice.amulet.RewardCouponV2.COMPANION)(co => co.payload.dso == dso) { contract => + DsoAcsStoreRowData( + contract, + rewardRound = Some(contract.payload.round.number), + rewardParty = Some( + PartyId.tryFromProtoPrimitive( + contract.payload.beneficiary.orElse(contract.payload.provider) + ) + ), + rewardAmount = Some(contract.payload.amount), + contractExpiresAt = Some(Timestamp.assertFromInstant(contract.payload.expiresAt)), + ) + }, + mkFilter(splice.amulet.rewardaccountingv2.CalculateRewardsV2.COMPANION)(co => + co.payload.dso == dso + ) { contract => + DsoAcsStoreRowData( + contract, + rewardRound = Some(contract.payload.round.number), + ) + }, + mkFilter(splice.amulet.rewardaccountingv2.ProcessRewardsV2.COMPANION)(co => + co.payload.dso == dso + ) { contract => + DsoAcsStoreRowData( + contract, + rewardRound = Some(contract.payload.round.number), + ) + }, mkFilter(splice.round.OpenMiningRound.COMPANION)(co => co.payload.dso == dso) { contract => DsoAcsStoreRowData( contract, diff --git a/build.sbt b/build.sbt index 9f8e7e60fc..f88b660102 100644 --- a/build.sbt +++ b/build.sbt @@ -111,6 +111,8 @@ lazy val root: Project = (project in file(".")) `splice-dso-governance-test-daml`, `splice-validator-lifecycle-daml`, `splice-validator-lifecycle-test-daml`, + `splice-api-featured-app-v1-daml`, + `splice-api-reward-assignment-v1-daml`, `splice-api-token-metadata-v1-daml`, `splice-api-token-holding-v1-daml`, `splice-api-token-transfer-instruction-v1-daml`, @@ -647,15 +649,18 @@ lazy val `splice-util-daml` = BuildCommon.damlSettings ) -lazy val `splice-featured-app-api-v1-daml` = +lazy val `splice-api-featured-app-v1-daml` = project .in(file("daml/splice-api-featured-app-v1")) .enablePlugins(DamlPlugin) .settings( - BuildCommon.damlSettings + BuildCommon.damlSettings, + // Exclude header check for FeaturedAppRightV1.daml as it exists on main without a header, + // and adding one would change its DAR hash, cascading through all dependent packages. + Compile / headerSources ~= { _.filterNot(_.getName == "FeaturedAppRightV1.daml") }, ) -lazy val `splice-featured-app-api-v2-daml` = +lazy val `splice-api-featured-app-v2-daml` = project .in(file("daml/splice-api-featured-app-v2")) .enablePlugins(DamlPlugin) @@ -663,6 +668,14 @@ lazy val `splice-featured-app-api-v2-daml` = BuildCommon.damlSettings ) +lazy val `splice-api-reward-assignment-v1-daml` = + project + .in(file("daml/splice-api-reward-assignment-v1")) + .enablePlugins(DamlPlugin) + .settings( + BuildCommon.damlSettings + ) + lazy val `splice-amulet-daml` = project .in(file("daml/splice-amulet")) @@ -677,8 +690,9 @@ lazy val `splice-amulet-daml` = (`splice-api-token-allocation-v1-daml` / Compile / damlBuild).value ++ (`splice-api-token-allocation-request-v1-daml` / Compile / damlBuild).value ++ (`splice-api-token-allocation-instruction-v1-daml` / Compile / damlBuild).value ++ - (`splice-featured-app-api-v1-daml` / Compile / damlBuild).value ++ - (`splice-featured-app-api-v2-daml` / Compile / damlBuild).value, + (`splice-api-reward-assignment-v1-daml` / Compile / damlBuild).value ++ + (`splice-api-featured-app-v1-daml` / Compile / damlBuild).value ++ + (`splice-api-featured-app-v2-daml` / Compile / damlBuild).value, ) lazy val `splice-amulet-test-daml` = @@ -781,8 +795,8 @@ lazy val `splice-util-featured-app-proxies-daml` = (`splice-api-token-transfer-instruction-v1-daml` / Compile / damlBuild).value ++ (`splice-api-token-allocation-v1-daml` / Compile / damlBuild).value ++ (`splice-api-token-allocation-instruction-v1-daml` / Compile / damlBuild).value ++ - (`splice-featured-app-api-v1-daml` / Compile / damlBuild).value ++ - (`splice-featured-app-api-v2-daml` / Compile / damlBuild).value, + (`splice-api-featured-app-v1-daml` / Compile / damlBuild).value ++ + (`splice-api-featured-app-v2-daml` / Compile / damlBuild).value, ) lazy val `splice-util-token-standard-wallet-daml` = @@ -795,8 +809,8 @@ lazy val `splice-util-token-standard-wallet-daml` = (`splice-api-token-holding-v1-daml` / Compile / damlBuild).value ++ (`splice-api-token-metadata-v1-daml` / Compile / damlBuild).value ++ (`splice-api-token-transfer-instruction-v1-daml` / Compile / damlBuild).value ++ - (`splice-featured-app-api-v1-daml` / Compile / damlBuild).value ++ - (`splice-featured-app-api-v2-daml` / Compile / damlBuild).value, + (`splice-api-featured-app-v1-daml` / Compile / damlBuild).value ++ + (`splice-api-featured-app-v2-daml` / Compile / damlBuild).value, ) lazy val `splice-util-featured-app-proxies-test-daml` = @@ -830,8 +844,8 @@ lazy val `splice-util-batched-markers-daml` = .settings( BuildCommon.damlSettings, Compile / damlDependencies := - (`splice-featured-app-api-v1-daml` / Compile / damlBuild).value ++ - (`splice-featured-app-api-v2-daml` / Compile / damlBuild).value, + (`splice-api-featured-app-v1-daml` / Compile / damlBuild).value ++ + (`splice-api-featured-app-v2-daml` / Compile / damlBuild).value, ) lazy val `splice-util-batched-markers-test-daml` = @@ -921,8 +935,9 @@ lazy val `apps-common` = `splice-api-token-allocation-instruction-v1-daml`, `splice-token-test-dummy-holding-daml`, `splice-token-test-trading-app-daml`, - `splice-featured-app-api-v1-daml`, - `splice-featured-app-api-v2-daml`, + `splice-api-featured-app-v1-daml`, + `splice-api-featured-app-v2-daml`, + `splice-api-reward-assignment-v1-daml`, `splice-util-batched-markers-daml`, ) .enablePlugins(BuildInfoPlugin) @@ -1981,8 +1996,9 @@ lazy val `apps-dar-resources-generator` = `splice-api-token-allocation-instruction-v1-daml`, `splice-token-test-dummy-holding-daml`, `splice-token-test-trading-app-daml`, - `splice-featured-app-api-v1-daml`, - `splice-featured-app-api-v2-daml`, + `splice-api-featured-app-v1-daml`, + `splice-api-featured-app-v2-daml`, + `splice-api-reward-assignment-v1-daml`, `splice-util-batched-markers-daml`, ) .settings( diff --git a/daml/dars.lock b/daml/dars.lock index 03c68625ba..aab97dc881 100644 --- a/daml/dars.lock +++ b/daml/dars.lock @@ -8,6 +8,7 @@ splice-amulet 0.1.14 3ca1343ab26b453d38c8adb70dca5f1ead8440c42b59b68f070786955cb splice-amulet 0.1.15 67fac2f853bce8dbf0b9817bb5ba7c59f10e8120b7c808696f7010e5f0c8a791 splice-amulet 0.1.16 c208d7ead1e4e9b610fc2054d0bf00716144ad444011bce0b02dcd6cd0cb8a23 splice-amulet 0.1.17 6c5802f86709a0ad4784af81f0bab40f3070b2f58128d8843da1e1784c147802 +splice-amulet 0.1.18 c08324c9abe47f07abceb3eb73e7dc043ccc8d1e52ecfb7c01e3edd6083d4d75 splice-amulet 0.1.2 1446ffdf23326cef2de97923df96618eb615792bea36cf1431f03639448f1645 splice-amulet 0.1.3 0d89016d5a90eb8bced48bbac99e81c57781b3a36094b8d48b8e4389851e19af splice-amulet 0.1.4 a36ef8888fb44caae13d96341ce1fabd84fc9e2e7b209bbc3caabb48b6be1668 @@ -27,6 +28,7 @@ splice-amulet-name-service 0.1.15 d4724b90dce9fb08badbb367962d237710b3a603e4f578 splice-amulet-name-service 0.1.16 53468a38bce11b51cd2ed10b9c09301c0b73570b50896d5649c4629de15815a3 splice-amulet-name-service 0.1.17 bcc80dce253c7b89efd9b263be5260a9609f8cb1fb5ea6e9916f6904552bdc82 splice-amulet-name-service 0.1.18 64232089d6dc6ae1eabcebcbe5e8b1aa8f413e9c57e8986d2bca883cc306fde2 +splice-amulet-name-service 0.1.19 22c55bc7a6f096a288f6eb37c4f7a270ddf24515592a5f5208ebef48c08834d6 splice-amulet-name-service 0.1.2 711a2974d65e6ebd149704da75f3f71234798687ab895b92f066c865dbdeeabb splice-amulet-name-service 0.1.3 beb4b85f3f0cf36dfb93fc917d3ac218ee5d41b6e70604720cb228d85e168ee0 splice-amulet-name-service 0.1.4 053c7f4c2a77312e7d465a4fa7dc8cb298754ad12c0c987a7c401bd724e65efc @@ -35,10 +37,11 @@ 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.21 b120f1061b3a4b278badcc27d2ab40bf9f5593a65b92fd1faa568ff02f6eba62 -splice-amulet-test 0.1.20 a9850c98ef59f0550ef7e036d11464bb8b7e97c4b7e88c8a390bbd27e946cca5 +splice-amulet-name-service-test 0.1.22 ad131644940a2aed40e47a9d25de77a81870dc3d3d13bd917cbff7c41d707b2b +splice-amulet-test 0.1.21 e735df1948468705fe7e7e851011a9ee4e5931f13e18225ee565a20ae5fcecbb splice-api-featured-app-v1 1.0.0 7804375fe5e4c6d5afe067bd314c42fe0b7d005a1300019c73154dd939da4dda splice-api-featured-app-v2 1.0.0 dd22e3e168a8c7fd0313171922dabf1f7a3b131bd9bfc9ff98e606f8c57707ea +splice-api-reward-assignment-v1 1.0.0 fd02ac68f16c7e9ef0c1ce039aaa0a2ee8e05cefb0d1e2703849175ca32c1ff2 splice-api-token-allocation-instruction-v1 1.0.0 275064aacfe99cea72ee0c80563936129563776f67415ef9f13e4297eecbc520 splice-api-token-allocation-request-v1 1.0.0 6fe848530b2404017c4a12874c956ad7d5c8a419ee9b040f96b5c13172d2e193 splice-api-token-allocation-v1 1.0.0 93c942ae2b4c2ba674fb152fe38473c507bda4e82b4e4c5da55a552a9d8cce1d @@ -66,6 +69,7 @@ splice-dso-governance 0.1.21 2d306cfe8cdb3daf2d21f84dfecc3e2f26a41504e58fe25cb7f splice-dso-governance 0.1.22 5c28530209b9ab37c5f187132cd826709bb18b0efe28411488ab750870414738 splice-dso-governance 0.1.23 0c94a036ac5168a1dee26b435838e062f0d2f47d6eac49303978228ae559edb9 splice-dso-governance 0.1.24 9f2753692448681cb518956778271de83309c654ac69b8d34a9f966633918669 +splice-dso-governance 0.1.25 fe580d120366ee3d978d7fff5348125c9a863de5317b1d4aba651b09abc16884 splice-dso-governance 0.1.3 b0ae3cc03e418790305a3c15f761fe495572de5827f8d322fb8b96996b783c13 splice-dso-governance 0.1.4 dc24fd18b4d151cd1e0ff6bfb7438bafb2f50fe076d0f16f50565e60b153a0be splice-dso-governance 0.1.5 9e3ca1d22ad495dfabf3d61acae3dc1a7718f527f02092280b58cf69edfdc84c @@ -73,8 +77,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.28 21e0ce51a5c919ee9c0aee42f1195c29e78b668ab068ac7b64ceb70b80264402 -splice-token-standard-test 1.0.11 b641f7509aff67cdfa82b66ebccfff072b2226b39e5bd998ac118570c926fc50 +splice-dso-governance-test 0.1.29 3c704210a145484f607305b37b229d933fc6db06aae9685c6b0b03e918c3abc7 +splice-token-standard-test 1.0.12 f1dd875da1ebabb1c67b9d1217b51f5dcbb2d9b74288c1333928215c5193bce4 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 @@ -86,15 +90,15 @@ splice-util 0.1.4 b7356fbb2cf8a3b22194d8c743c3c216d9c7527b257c8c38b257eb22942be3 splice-util 0.1.5 5a58024e2cc488ca9e0c952ec7ef41da3a1ed0a78ba23bacd819e5b30afb5546 splice-util-batched-markers 1.0.0 727c5e97457d3ff841680816eb70d55834827ef756bac8551cace5b961c9c1d2 splice-util-batched-markers 1.0.1 4d91a9b044e0e996e91ee9aac3442591ffc78f16da4ff5c6f55218ba667f6192 -splice-util-batched-markers-test 1.0.3 b40181117be76fdc157a03c35ef13af3c27833ebd2d16e5b4e74843d9542f9aa +splice-util-batched-markers-test 1.0.4 196cf93400e5e72b2dddee8d46334818aefe8ee408581523d8e22edecfa551e5 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 1.2.1 06bab917848ef275317c2539b75c23b94e03ceb55b4a1346936f7832084cd7a6 splice-util-featured-app-proxies 1.2.2 2889c094cf9678b2b666221934ea56ab169a31b257450845bd53217a8cdfe44f -splice-util-featured-app-proxies-test 1.0.9 3a0681eabdf05b8d953e129990df79473ea585815d5dc42c211db1cb1d1f1e79 +splice-util-featured-app-proxies-test 1.0.10 05287b35e916ef48b494f7579f5682bcc412408b96049e393cf2cbf3c840ba54 splice-util-token-standard-wallet 1.0.0 1da198cb7968fa478cfa12aba9fdf128a63a8af6ab284ea6be238cf92a3733ac -splice-util-token-standard-wallet-test 1.0.4 bc8884d5e66750b5492cf5f0b0a3889b8d83ecb98759f0c980b9974ae42d6fdc +splice-util-token-standard-wallet-test 1.0.5 ca3cfab860f9712e2b22ef507b27b2b3c1cbb4849eb2bda04bfb0563c41dc8b9 splice-validator-lifecycle 0.1.0 cef96fac957362f1fc097120bd13686cac7f84fbc8053afa994a1f9214d9570c splice-validator-lifecycle 0.1.1 1ddf05c96002914593c929848b786f34c753fb0be07717d1786be177a564aada splice-validator-lifecycle 0.1.2 57e2f15f9755db1f00e51c52c319294264a21ad71c6bc1e7cd70db4b164c0aaa @@ -114,6 +118,7 @@ splice-wallet 0.1.15 fd57252dda29e3ce90028114c91b521cb661df5a9d6e87c41a9e9151821 splice-wallet 0.1.16 17dca10fd8eb6a833be530fe9c6f9c2b7397a38c06e9c86d0679adc200b90e14 splice-wallet 0.1.17 176c2924cd7aa12bc81ffd1a8d6cfaf46e70378f653eb5f19f2d6b9599cfd45c splice-wallet 0.1.18 94d88246f69d8a4b69333d1f993e3280deaca19b70511ea7687f01e4328a34a4 +splice-wallet 0.1.19 5b8a0c260b0aff8c6751eb5c46595276089632e7ddbf91aa3ba8dcc3e4c54401 splice-wallet 0.1.2 c162e08a4ec0428bfa870b6d9040989e575c74199c3a80558c62e03196dd5146 splice-wallet 0.1.3 2c35bb4f5084ea66db59717d21750bfd64c43147ef5fd5166615092d592a6917 splice-wallet 0.1.4 141dad2d33b6410b8e1c35a0c4f8f76cb691e4d9a4410ce89f33f373855317e1 @@ -132,6 +137,7 @@ splice-wallet-payments 0.1.14 45b29d6e05b5352c39edde850c66b4535c682b9991b06eec31 splice-wallet-payments 0.1.15 f80fae7a9de9431854372a66c3ca78675f77b2f54ede65abdc1b1abdec707d21 splice-wallet-payments 0.1.16 45e7ac4601186747e2c4d2fd7e54a15e5752eee56d6cf767eb62141b7a10c0a8 splice-wallet-payments 0.1.17 94bba10a5b3fef448ccd28669359af3b09442a1d1bd6cdbb52c401d7d10075bc +splice-wallet-payments 0.1.18 d394954c7a91048206ae53f92b36b49c1d322a3d63d3e234e14023334dec21ba splice-wallet-payments 0.1.2 775f5eb9c0249509adda5eb3ea4ee31bb953601168c18880df6f2ff09ec4298a splice-wallet-payments 0.1.3 b953b3729c81a55e598a364be7d0c0574750df3de12a7a1b53a300f217cb5c5c splice-wallet-payments 0.1.4 12177f54873c1094ea169874ad0d7838383fd137f302d16356e93f28dfbc0fcc @@ -140,7 +146,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.21 c751f85e42e928f0e300d561bda15be7bb42dd19681a52a4955d839f47f6b695 +splice-wallet-test 0.1.22 6aa39655a6a1eb5ebb0d7ff5338ce887334011a0c0db2cbce1f8edda392b132a splitwell 0.1.0 075c76de553ab88383a7c69de134afa82aacfdf8ea8fcfe8852c4b199c3b2669 splitwell 0.1.1 ccb1a0215053062202052e1a052f9214da3fdae5253a6d43e2e155ff4f57fe75 splitwell 0.1.10 d42676a366f7ca7a2409974dd3054aa4d83ab29baa3b2086ad021407b0a1a295 @@ -152,6 +158,7 @@ splitwell 0.1.15 2f3d8a50f57e66af450c36556a09d04c1d9117b699720118b7bd30255680549 splitwell 0.1.16 2c8567bc0e7cd15d29de8dcbc8d992aa7a42a3805e9831765d670b03c7c2474a splitwell 0.1.17 a631654e66ef31017bf3c9cb4ab2429157d5e5f948f1b6b15a38f0ec7c0cd363 splitwell 0.1.18 4694a5545800c7b98cdd7e7349c98f037931bb91574a76715d52da9c647c4081 +splitwell 0.1.19 2337bd0912c82be5b3dd7512ba0b9caa9b63e50b0e375687648cc255849bd4e3 splitwell 0.1.2 778edd2c228c6b68198d4d033885b2d0dae7daaee55d7df3edd9dfdf1f10fbd0 splitwell 0.1.3 7cde068cde689584f86a2499689d5cb165264d96496721e24ac6fb909f770a58 splitwell 0.1.4 85557b86cd4f330f093915db1ea26eac5092de6b5ddae0690146f6059c89419b @@ -160,4 +167,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.21 7cf052802bd7c9f3dcbe7f7af893e595310f7ec98fee05362208cb4808b3b61f \ No newline at end of file +splitwell-test 0.1.22 644b5bebcc4ad96535c3d584eb90831f21452d6e59f5319204dd6b689b4d3958 \ No newline at end of file diff --git a/daml/dars/splice-amulet-0.1.18.dar b/daml/dars/splice-amulet-0.1.18.dar new file mode 100644 index 0000000000..de7bd9ba90 Binary files /dev/null and b/daml/dars/splice-amulet-0.1.18.dar differ diff --git a/daml/dars/splice-amulet-name-service-0.1.19.dar b/daml/dars/splice-amulet-name-service-0.1.19.dar new file mode 100644 index 0000000000..f9e9313765 Binary files /dev/null and b/daml/dars/splice-amulet-name-service-0.1.19.dar differ diff --git a/daml/dars/splice-api-reward-assignment-v1-1.0.0.dar b/daml/dars/splice-api-reward-assignment-v1-1.0.0.dar new file mode 100644 index 0000000000..a33d46237f Binary files /dev/null and b/daml/dars/splice-api-reward-assignment-v1-1.0.0.dar differ diff --git a/daml/dars/splice-dso-governance-0.1.25.dar b/daml/dars/splice-dso-governance-0.1.25.dar new file mode 100644 index 0000000000..6618bee554 Binary files /dev/null and b/daml/dars/splice-dso-governance-0.1.25.dar differ diff --git a/daml/dars/splice-wallet-0.1.19.dar b/daml/dars/splice-wallet-0.1.19.dar new file mode 100644 index 0000000000..50e678c708 Binary files /dev/null and b/daml/dars/splice-wallet-0.1.19.dar differ diff --git a/daml/dars/splice-wallet-payments-0.1.18.dar b/daml/dars/splice-wallet-payments-0.1.18.dar new file mode 100644 index 0000000000..fd6e5fd656 Binary files /dev/null and b/daml/dars/splice-wallet-payments-0.1.18.dar differ diff --git a/daml/dars/splitwell-0.1.19.dar b/daml/dars/splitwell-0.1.19.dar new file mode 100644 index 0000000000..1477d464c4 Binary files /dev/null and b/daml/dars/splitwell-0.1.19.dar differ diff --git a/daml/splice-amulet-name-service-test/daml.yaml b/daml/splice-amulet-name-service-test/daml.yaml index d4dfda48ca..22ab8c22be 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.21 +version: 0.1.22 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-amulet-name-service/daml.yaml b/daml/splice-amulet-name-service/daml.yaml index 2060d08ef1..f4a6b2b7ea 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.18 +version: 0.1.19 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-amulet-name-service/daml/Splice/Ans/AmuletConversionRateFeed.daml b/daml/splice-amulet-name-service/daml/Splice/Ans/AmuletConversionRateFeed.daml index 7a03e54a97..2993386a34 100644 --- a/daml/splice-amulet-name-service/daml/Splice/Ans/AmuletConversionRateFeed.daml +++ b/daml/splice-amulet-name-service/daml/Splice/Ans/AmuletConversionRateFeed.daml @@ -9,10 +9,12 @@ -- is a long-standing plan. module Splice.Ans.AmuletConversionRateFeed where +import DA.Action (when) import DA.Assert import DA.Foldable import DA.Time import Splice.AmuletRules +import Splice.AmuletConfig import Splice.Api.FeaturedAppRightV1 qualified as Api import Splice.Schedule import Splice.Types @@ -58,11 +60,12 @@ template AmuletConversionRateFeed -- always be slightly slower than the minimum rate limit. -- we add one extra microseconds so now + 5 minutes is allowed and the caller does not need to add the microsecond assertWithinDeadline "newNextUpdateAfter - 0.5 tickDuration" (newNextUpdateAfter `addRelTime` convertMicrosecondsToRelTime (- (convertRelTimeToMicroseconds currentConfig.tickDuration) / 2 + 1)) - forA_ markerContextO $ \markerContext -> do - _ <- fetchCheckedInterface ForOwner{dso, owner = publisher} markerContext.featuredAppRightCid - exercise markerContext.featuredAppRightCid Api.FeaturedAppRight_CreateActivityMarker - with - beneficiaries = markerContext.beneficiaries + when (useFeaturedAppMarkers $ fmap (.mintingVersion) currentConfig.rewardConfig) $ + forA_ markerContextO $ \markerContext -> do + _ <- fetchCheckedInterface ForOwner{dso, owner = publisher} markerContext.featuredAppRightCid + exercise markerContext.featuredAppRightCid Api.FeaturedAppRight_CreateActivityMarker + with + beneficiaries = markerContext.beneficiaries cid <- create this with amuletConversionRate nextUpdateAfter = Some newNextUpdateAfter diff --git a/daml/splice-amulet-test/daml.yaml b/daml/splice-amulet-test/daml.yaml index c739b84549..82ff896441 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.20 +version: 0.1.21 dependencies: - daml-prim - daml-stdlib @@ -15,6 +15,7 @@ data-dependencies: - ../splice-util/.daml/dist/splice-util-current.dar - ../splice-amulet/.daml/dist/splice-amulet-current.dar - ../splice-api-featured-app-v1/.daml/dist/splice-api-featured-app-v1-current.dar + - ../splice-api-reward-assignment-v1/.daml/dist/splice-api-reward-assignment-v1-current.dar - ../../token-standard/splice-api-token-metadata-v1/.daml/dist/splice-api-token-metadata-v1-current.dar - ../../token-standard/splice-api-token-holding-v1/.daml/dist/splice-api-token-holding-v1-current.dar - ../../token-standard/splice-api-token-transfer-instruction-v1/.daml/dist/splice-api-token-transfer-instruction-v1-current.dar diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/TestLockAndAmuletExpiry.daml b/daml/splice-amulet-test/daml/Splice/Scripts/TestLockAndAmuletExpiry.daml index 71794ecc7e..deb500bda6 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/TestLockAndAmuletExpiry.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/TestLockAndAmuletExpiry.daml @@ -34,6 +34,7 @@ scaleAmuletConfig amuletPrice config = AmuletConfig with featuredAppActivityMarkerAmount = fmap (/ amuletPrice) config.featuredAppActivityMarkerAmount optDevelopmentFundManager = config.optDevelopmentFundManager externalPartyConfigStateTickDuration = config.externalPartyConfigStateTickDuration + rewardConfig = config.rewardConfig test : Script () test = script do diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/TestRewardAccountingV2.daml b/daml/splice-amulet-test/daml/Splice/Scripts/TestRewardAccountingV2.daml new file mode 100644 index 0000000000..a3bb753436 --- /dev/null +++ b/daml/splice-amulet-test/daml/Splice/Scripts/TestRewardAccountingV2.daml @@ -0,0 +1,569 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +module Splice.Scripts.TestRewardAccountingV2 where + + +import DA.Action (void) +import DA.Assert +import DA.Foldable (forA_) +import DA.List +import DA.Map qualified as Map +import DA.Set as Set +import DA.Time + +import Daml.Script + +import Splice.Api.RewardAssignmentV1 qualified as Api.RewardAssignmentV1 + +import Splice.Amulet +import Splice.Amulet.RewardAccountingV2 +import Splice.Amulet.CryptoHash qualified as CryptoHash +import Splice.AmuletConfig +import Splice.AmuletRules +import Splice.ExternalPartyConfigState +import Splice.Round +import Splice.Types + +import Splice.Scripts.Util + +import Splice.Testing.Registries.AmuletRegistry.Parameters (defaultAmuletConfig) +import Splice.Testing.TokenStandard.WalletClient as WalletClient +import Splice.Testing.Utils + + +-- Reward accounting tests +--------------------------- + +-- | Shared function to test the creation and processing of reward batches in dry-run or production mode. +create_and_process_reward_batches : Bool -> Script (Script (), (AmuletApp, Party, Party, Party, Party)) +create_and_process_reward_batches dryRun = do + -- enable traffic based app rewards, which are the first use-case for reward accounting v2 + app <- setupAppWithConfig $ defaultAmuletConfig with + rewardConfig = Some $ RewardConfig with + mintingVersion = if dryRun then RewardVersion_FeaturedAppMarkers else RewardVersion_TrafficBasedAppRewards + dryRunVersion = if dryRun then Some RewardVersion_TrafficBasedAppRewards else None + batchSize = 100 + rewardCouponTimeToLive = hours 36 + appRewardCouponThreshold = 0.5 + + -- setup users + alice <- allocateParty "Alice" + bob <- allocateParty "Bob" + charlie <- allocateParty "Charlie" + dora <- allocateParty "Dora" + + setTime demoTime + + -- move the first round through issuance, which will also trigger the reward calculation for this round + if dryRun then runNextIssuance app + else runNextIssuanceWithTrafficBasedAppRewards app 1.2 + + -- setup demo data + let mintingAllowances1 = sortOn (.provider) + [ MintingAllowance alice 1000.0 + , MintingAllowance bob 2000.0 + ] + let mintingAllowances2 = sortOn (.provider) + [ MintingAllowance charlie 30.0 + , MintingAllowance dora 5.1 + ] + let b1 = BatchOfMintingAllowances mintingAllowances1 + let b2 = BatchOfMintingAllowances mintingAllowances2 + let rootBatch = BatchOfBatches [CryptoHash.hash b1, CryptoHash.hash b2] + let rootBatchHash = CryptoHash.hash rootBatch + let batchesWithHiding = [(b1, [bob]), (b2, [dora]), (rootBatch, [])] + + -- get the contract representing the pending calculation and confirmation of rewards for round 0 + [(calculateRewardsCid, _)] <- query @CalculateRewardsV2 app.dso + + [(amuletRulesCid, _)] <- query @AmuletRules app.dso + + -- setup reward coupon creation workflow state + submit [app.dso] $ exerciseCmd amuletRulesCid + AmuletRules_StartProcessingRewardsV2 with + calculateRewardsCid + batchHash = rootBatchHash + + let processBatches = do + states <- query @ProcessRewardsV2 app.dso + forA_ states $ \(processRewardsCid, processRewards) -> do + let Some (b, badVettingState) = find (\(b, _) -> CryptoHash.hash b == processRewards.batchHash) batchesWithHiding + void $ submit app.dso $ exerciseCmd processRewardsCid + ProcessRewardsV2_ProcessBatch with + batch = b + providersWithWrongVettingState = Set.fromList badVettingState + + pure (processBatches, (app, alice, bob, charlie, dora)) + +-- | Test the full happy path of reward accounting v2 +test_reward_accounting_v2 : Script () +test_reward_accounting_v2 = do + (processBatches, (app, alice, bob, charlie, dora)) <- create_and_process_reward_batches False + + -- show that a non-dry run cannot be archived + processRewards <- query @ProcessRewardsV2 app.dso + calculateRewards <- query @CalculateRewardsV2 app.dso + [(amuletRulesCid, _)] <- query @AmuletRules app.dso + + submitMustFail app.dso $ exerciseCmd amuletRulesCid AmuletRules_ArchiveDryRunRewardAccountingV2 with + processRewardsCids = map fst processRewards + calculateRewardsCids = map fst calculateRewards + + -- proceed with processing + processBatches -- expand root hash into batch hashes + processBatches -- expand follow-up batches into coupons + + -- no left-over processing contracts + [] <- query @CalculateRewardsV2 app.dso + [] <- query @ProcessRewardsV2 app.dso + + -- check created coupons + now <- getTime + let couponExpiryTime = now `addRelTime` hours 36 + let expectedAmounts = [(alice, 1000.0), (bob, 2000.0), (charlie, 30.0), (dora, 5.1)] + let expectedCoupons = do + (provider, amount) <- expectedAmounts + pure RewardCouponV2 with + dso = app.dso + provider + amount + round = Round 0 + expiresAt = couponExpiryTime + providerIsObserver = provider `notElem` [bob, dora] + beneficiary = None + + actualCoupons0 <- query @RewardCouponV2 app.dso + let actualCoupons = sortOn (.provider) $ fmap snd actualCoupons0 + actualCoupons === expectedCoupons + + -- make Bob and Dora observers of their coupons (simulates them changing their vetting state) + [(amuletRulesCid, _)] <- query @AmuletRules app.dso + unobservableCoupons <- queryFilter @RewardCouponV2 app.dso (\c -> not c.providerIsObserver) + void $ submit app.dso $ exerciseCmd amuletRulesCid AmuletRules_UnhideRewardCouponsV2 with + rewardCouponCids = map fst unobservableCoupons + beneficiaries = map (._2.provider) unobservableCoupons + + couponsAfterUnhiding <- query @RewardCouponV2 app.dso + sortOn (.provider) (map snd couponsAfterUnhiding) === + map (\c -> c with providerIsObserver = True) expectedCoupons + + -- check that reward minting works + forA_ expectedAmounts $ \(beneficiary, amount) -> do + mintRewardsV2 app beneficiary + WalletClient.checkBalance beneficiary app.registry.instrumentId amount + + pure () + + +test_reward_accounting_v2_dry_run : Script () +test_reward_accounting_v2_dry_run = do + (processBatches, (app, _, _, _, _)) <- create_and_process_reward_batches True + + processBatches -- expand root hash into batch hashes + processBatches -- expand follow-up batches into coupons + + -- no left-over processing contracts + [] <- query @CalculateRewardsV2 app.dso + [] <- query @ProcessRewardsV2 app.dso + + -- check that no coupons were created + [] <- query @RewardCouponV2 app.dso + pure () + +test_reward_accounting_v2_skip_stuck_dry_run : Script () +test_reward_accounting_v2_skip_stuck_dry_run = do + (processBatches, (app, _, _, _, _)) <- create_and_process_reward_batches True + + processBatches -- expand root hash into batch hashes + -- pretend that follow up batches fail to process due to hash mismatches + + -- move to next round, so there's also a calculate rewards contract + runNextIssuance app + + -- archive the stuck state + processRewards <- query @ProcessRewardsV2 app.dso + calculateRewards <- query @CalculateRewardsV2 app.dso + [(amuletRulesCid, _)] <- query @AmuletRules app.dso + + submit app.dso $ exerciseCmd amuletRulesCid AmuletRules_ArchiveDryRunRewardAccountingV2 with + processRewardsCids = map fst processRewards + calculateRewardsCids = map fst calculateRewards + + -- no left-over processing contracts + [] <- query @CalculateRewardsV2 app.dso + [] <- query @ProcessRewardsV2 app.dso + + -- check that no coupons were created + [] <- query @RewardCouponV2 app.dso + pure () + +-- | When not running in dry-run mode, the contracts tracking the processing cannot be archived +test_reward_accounting_v2_only_archive_dry_run : Script () +test_reward_accounting_v2_only_archive_dry_run = do + (processBatches, (app, _, _, _, _)) <- create_and_process_reward_batches False + + processBatches -- expand root hash into batch hashes + -- pretend that follow up batches fail to process due to hash mismatches + + -- move to next round, so there's also a calculate rewards contract + runNextIssuanceWithTrafficBasedAppRewards app 1.2 + + -- show that a non-dry run cannot be archived + processRewards <- query @ProcessRewardsV2 app.dso + calculateRewards <- query @CalculateRewardsV2 app.dso + [(amuletRulesCid, _)] <- query @AmuletRules app.dso + + submitMustFail app.dso $ exerciseCmd amuletRulesCid AmuletRules_ArchiveDryRunRewardAccountingV2 with + processRewardsCids = map fst processRewards + calculateRewardsCids = [] + + submitMustFail app.dso $ exerciseCmd amuletRulesCid AmuletRules_ArchiveDryRunRewardAccountingV2 with + processRewardsCids = [] + calculateRewardsCids = map fst calculateRewards + + pure () + + +-- Reward minting +----------------- + +data SetupConfig = SetupConfig with + hideCoupon : Bool -- ^ Whether to hide alice's coupon + useTrafficBasedAppRewards : Bool -- ^ Whether to configure traffic-based rewards for minting + +setupAliceWithCoupon : Bool -> Script (AmuletApp, Party, AmuletUser) +setupAliceWithCoupon hideCoupon = setupAliceWithCoupon' $ SetupConfig with + hideCoupon + useTrafficBasedAppRewards = False + +setupAliceWithCoupon' : SetupConfig -> Script (AmuletApp, Party, AmuletUser) +setupAliceWithCoupon' config = do + app <- setupAppWithConfig $ defaultAmuletConfig with + rewardConfig = Some $ RewardConfig with + mintingVersion = + if config.useTrafficBasedAppRewards + then RewardVersion_TrafficBasedAppRewards else RewardVersion_FeaturedAppMarkers + dryRunVersion = None + batchSize = 100 + rewardCouponTimeToLive = hours 36 + appRewardCouponThreshold = 0.5 + setTime demoTime + aliceUser <- setupUserWithoutValidatorRight app "Alice" + let alice = aliceUser.primaryParty + + -- bare create a coupon + let coupon = RewardCouponV2 with + dso = app.dso + provider = alice + amount = 1000.0 + round = Round 0 + expiresAt = demoTime `addRelTime` hours 36 + providerIsObserver = not config.hideCoupon + beneficiary = None + submit app.dso $ createCmd coupon + pure (app, alice, aliceUser) + + +test_direct_mint : Script () +test_direct_mint = do + (app, alice, _) <- setupAliceWithCoupon False + + -- mint and check balance + mintRewardsV2 app alice + WalletClient.checkBalance alice app.registry.instrumentId 1000.0 + + +test_mint_of_hidden_coupon : Script () +test_mint_of_hidden_coupon = do + (app, alice, _) <- setupAliceWithCoupon True + + -- create transfer context + (openMiningRound, _) <- getLatestOpenRound app + let context = TransferContext with + openMiningRound + issuingMiningRounds = Map.empty + validatorRights = Map.empty + featuredAppRight = None + -- mint the coupons + coupons <- query @RewardCouponV2 app.dso + [(amuletRulesCid, _)] <- query @AmuletRules app.dso + submitMulti [alice] [app.dso] $ exerciseCmd amuletRulesCid AmuletRules_Transfer with + transfer = Transfer with + sender = alice + provider = alice + inputs = map (InputRewardCouponV2 . fst) coupons + outputs = [] + beneficiaries = None -- no featured-app-marker beneficiaries + context + expectedDso = Some app.dso + + -- check balance + WalletClient.checkBalance alice app.registry.instrumentId 1000.0 + + +-- Coupon assignment +-------------------- + +test_coupon_assignment : Script () +test_coupon_assignment = testCouponAssignment False + +test_hidden_coupon_assignment : Script () +test_hidden_coupon_assignment = testCouponAssignment True + +testCouponAssignment : Bool -> Script () +testCouponAssignment hideCoupon = do + (app, alice, _) <- setupAliceWithCoupon hideCoupon + bob <- allocateParty "Bob" + charlie <- allocateParty "Charlie" + + -- get the original coupon before creating the extra one + let queryParty = if hideCoupon then app.dso else alice + [(couponCid, _)] <- queryInterface @Api.RewardAssignmentV1.RewardCoupon queryParty + + -- create a second coupon with a different amount and round + extraCouponCid <- submit app.dso $ createCmd RewardCouponV2 with + dso = app.dso + provider = alice + amount = 500.0 + round = Round 1 + expiresAt = demoTime `addRelTime` hours 36 `addRelTime` minutes 10 + providerIsObserver = not hideCoupon + beneficiary = None + + -- assign both coupons to bob, alice, and charlie + let weights = [(bob, 0.3), (alice, 0.5), (charlie, 0.2)] + Api.RewardAssignmentV1.RewardCoupon_AssignBeneficiariesResult{..} <- + submitMulti [alice] [app.dso] $ exerciseCmd couponCid Api.RewardAssignmentV1.RewardCoupon_AssignBeneficiaries with + additionalCoupons = [toInterfaceContractId extraCouponCid] + newBeneficiaries = map (uncurry Api.RewardAssignmentV1.RewardBeneficiary) weights + extraArgs = emptyExtraArgs + + -- original coupons are archived after assignment + None <- queryContractId alice extraCouponCid + + -- both coupons are split by the same weights, preserving their original round and expiresAt + let round0Expiry = demoTime `addRelTime` hours 36 + round1Expiry = demoTime `addRelTime` hours 36 `addRelTime` minutes 10 + couponSpecs = [(Round 0, 1000.0, round0Expiry), (Round 1, 500.0, round1Expiry)] + let mkExpected beneficiary round expiresAt amount = RewardCouponV2 with + dso = app.dso + provider = alice + amount + round + expiresAt + providerIsObserver = True + beneficiary = Some beneficiary + forA_ newBeneficiariesCouponCids $ \(beneficiary, cids) -> do + let Some (_, weight) = find (\(p, _) -> p == beneficiary) weights + -- each beneficiary gets one coupon per original coupon + length cids === 2 + forA_ (zip cids couponSpecs) $ \(cid, (round, couponAmount, expiresAt)) -> do + Some coupon <- queryContractId beneficiary (fromInterfaceContractId @RewardCouponV2 cid) + coupon === mkExpected beneficiary round expiresAt (couponAmount * weight) + + -- verify assigned coupons cannot be further assigned by provider + let Some (_, bobCouponCid :: _) = find (\(p, _) -> p == bob) newBeneficiariesCouponCids + submitMustFail alice $ exerciseCmd bobCouponCid Api.RewardAssignmentV1.RewardCoupon_AssignBeneficiaries with + additionalCoupons = [] + newBeneficiaries = [Api.RewardAssignmentV1.RewardBeneficiary charlie 1.0] + extraArgs = emptyExtraArgs + -- nor by the beneficiary + submitMustFail bob $ exerciseCmd bobCouponCid Api.RewardAssignmentV1.RewardCoupon_AssignBeneficiaries with + additionalCoupons = [] + newBeneficiaries = [Api.RewardAssignmentV1.RewardBeneficiary charlie 1.0] + extraArgs = emptyExtraArgs + + +-- Claiming expired reward coupons +---------------------------------- + +test_claim_expired_coupons : Script () +test_claim_expired_coupons = do + (app, alice, _) <- setupAliceWithCoupon False + bob <- allocateParty "Bob" + charlie <- allocateParty "Charlie" + + -- create an extra coupon + let extraCoupon = RewardCouponV2 with + dso = app.dso + provider = alice + amount = 500.0 + round = Round 0 + expiresAt = demoTime `addRelTime` hours 48 + providerIsObserver = False + beneficiary = Some bob + submit app.dso $ createCmd extraCoupon + + -- show that expiry doesn't work before expiry time + coupons <- query @RewardCouponV2 app.dso + let rewardCouponCids = map fst coupons + length coupons === 2 + + [(amuletRulesCid, _)] <- query @AmuletRules app.dso + + submitMustFail app.dso $ exerciseCmd amuletRulesCid AmuletRules_ClaimExpiredRewardsV2 with + rewardCouponCids + beneficiaries = [bob, alice] + + -- move time past expiry of the first coupon + passTime $ hours 48 + + -- beneficiaries are checked + submitMustFail app.dso $ exerciseCmd amuletRulesCid AmuletRules_ClaimExpiredRewardsV2 with + rewardCouponCids + beneficiaries = [] + + submitMustFail app.dso $ exerciseCmd amuletRulesCid AmuletRules_ClaimExpiredRewardsV2 with + rewardCouponCids + beneficiaries = [alice, bob, charlie] + + -- correct expiry works + submit app.dso $ exerciseCmd amuletRulesCid AmuletRules_ClaimExpiredRewardsV2 with + rewardCouponCids + beneficiaries = [bob, alice] + + -- no coupons left + [] <- query @RewardCouponV2 app.dso + + -- unclaimed reward was created + [(_, unclaimedReward)] <- query @UnclaimedReward app.dso + unclaimedReward === UnclaimedReward with + dso = app.dso + amount = 1500.0 + + pure () + + +-- test featured marker disablement +----------------------------------- + +test_markers_pre_traffic_based : Script () +test_markers_pre_traffic_based = do + (app, alice, aliceUser) <- setupAliceWithCoupon' $ SetupConfig with + hideCoupon = False + useTrafficBasedAppRewards = False + featureApp app aliceUser + bob <- allocateParty "Bob" + + -- make a transfer and check that the marker is created + pay app aliceUser bob 100.0 + [(_, marker)] <- query @FeaturedAppActivityMarker app.dso + marker === FeaturedAppActivityMarker with + dso = app.dso + provider = alice + beneficiary = alice + weight = 1.0 + + -- switch config and test that markers are properly archived + updateAmuletConfig app $ \config -> config with + rewardConfig = Some $ RewardConfig with + mintingVersion = RewardVersion_TrafficBasedAppRewards + dryRunVersion = None + batchSize = 100 + rewardCouponTimeToLive = hours 36 + appRewardCouponThreshold = 0.5 + + -- two issuance required to make the first round with the updated config active and open + runNextIssuance app + runNextIssuance app + + markers <- query @FeaturedAppActivityMarker app.dso + [(amuletRulesCid, _)] <- query @AmuletRules app.dso + (openMiningRoundCid, _) <- getLatestOpenRound app + + submit app.dso $ exerciseCmd amuletRulesCid AmuletRules_ConvertFeaturedAppActivityMarkers with + markerCids = map fst markers + openMiningRoundCid + observers = None + + [] <- query @FeaturedAppActivityMarker app.dso + [] <- query @AppRewardCoupon app.dso + + pure () + + +test_no_markers_post_traffic_based : Script () +test_no_markers_post_traffic_based = do + (app, _, aliceUser) <- setupAliceWithCoupon' $ SetupConfig with + hideCoupon = False + useTrafficBasedAppRewards = True + featureApp app aliceUser + bob <- allocateParty "Bob" + + -- pay and check that there is no marker + pay app aliceUser bob 100.0 + [] <- query @FeaturedAppActivityMarker app.dso + + pure () + + +-- test switch to traffic-based rewards +--------------------------------------- + +test_switch_to_traffic_based_rewards : Script () +test_switch_to_traffic_based_rewards = do + app <- setupApp + + -- validate starting config + let checkAmuletRules expectedRewardConfig = do + [(_, amuletRules)] <- query @AmuletRules app.dso + amuletRules.configSchedule.initialValue.rewardConfig === expectedRewardConfig + let checkOpenRounds expectedRewardConfig = do + openRounds <- query @OpenMiningRound app.dso + length openRounds === 3 + forA_ openRounds $ \(_, openRound) -> + openRound.rewardConfig === expectedRewardConfig + let checkSummarizingRounds expectedRewardConfig = do + summarizingRounds <- query @SummarizingMiningRound app.dso + forA_ summarizingRounds $ \(_, summarizingRound) -> + summarizingRound.rewardConfig === expectedRewardConfig + let checkExternalPartyConfigs expectedVersion = do + externalPartyConfigs <- query @ExternalPartyConfigState app.dso + length externalPartyConfigs === 2 + forA_ externalPartyConfigs $ \(_, config) -> + config.rewardCalculationVersion === expectedVersion + + checkAmuletRules None + checkOpenRounds None + checkExternalPartyConfigs None + + -- switch config + let rewardConfig = Some $ RewardConfig with + mintingVersion = RewardVersion_TrafficBasedAppRewards + dryRunVersion = None + batchSize = 100 + rewardCouponTimeToLive = hours 36 + appRewardCouponThreshold = 0.5 + updateAmuletConfig app $ \config -> config with rewardConfig + + -- validate updated config + checkAmuletRules rewardConfig + checkOpenRounds None + checkExternalPartyConfigs None + + -- advance to next round to see that it picks up the config change + runNextIssuance app + (_, latestRound) <- getLatestActiveOpenRound app + latestRound.rewardConfig === rewardConfig + + -- SummarizingRounds are still on old config + checkSummarizingRounds None + + -- external party updates pick up the new config from the round + passTime (hours 24) + updateExternalPartyConfigState app + -- require two updates as each only processes one of the two states + passTime (hours 24) + updateExternalPartyConfigState app + + checkExternalPartyConfigs ((.mintingVersion) <$> rewardConfig) + + -- two more issuances and all open rounds use the new config + runNextIssuance app + runNextIssuance app + + checkOpenRounds rewardConfig + checkSummarizingRounds rewardConfig + + pure () diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Amulet/CryptoHash.daml b/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Amulet/CryptoHash.daml new file mode 100644 index 0000000000..095ffc2f3c --- /dev/null +++ b/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Amulet/CryptoHash.daml @@ -0,0 +1,368 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +module Splice.Scripts.UnitTests.Amulet.CryptoHash where + +import Daml.Script +import DA.Assert +import Splice.Amulet.CryptoHash + +-------------------------------------------------------------------------------- +-- Record Types +-------------------------------------------------------------------------------- + +data RecV1 = RecV1 + with + a : Int + b : Text + deriving (Eq, Show) + +data RecV1' = RecV1' + with + a : Int + deriving (Eq, Show) + +data RecV2 = RecV2 + with + a : Int + b : Text + c : Optional Int + deriving (Eq, Show) + +data RecV3 = RecV3 + with + a : Int + b : Text + c : Optional Int + d : Optional Int + deriving (Eq, Show) + +instance Hashable RecV1 where + hash r = hashRecord [hash r.a , hash r.b] + +instance Hashable RecV1' where + hash r = hashRecord [hash r.a] + + +instance Hashable RecV2 where + hash r = + hashUpgradedRecord + [ hash r.a + , hash r.b ] + [ fmap hash r.c ] + +instance Hashable RecV3 where + hash r = + hashUpgradedRecord + [ hash r.a + , hash r.b + ] + [ fmap hash r.c + , fmap hash r.d + ] + + +-------------------------------------------------------------------------------- +-- Variant Payload Type (shared argument type) +-------------------------------------------------------------------------------- + +data Payload = Payload + with + x : Int + y : Text + deriving (Eq, Show) + + +-------------------------------------------------------------------------------- +-- Variant Types (same argument type via `with`) +-------------------------------------------------------------------------------- + +data VarV1 + = V1 with payload : Payload + | V2 with payload : Payload + deriving (Eq, Show) + +instance Hashable VarV1 where + hash v = + case v of + V1 with payload -> + hashVariant + "V1" + [ hash payload.x + , hash payload.y + ] + + V2 with payload -> + hashVariant + "V2" + [ hash payload.x + , hash payload.y + ] + + +data VarV2 + = V1V2 + with + payload : Payload + c : Optional Int + d : Optional Int + | V2V2 + with + payload : Payload + c : Optional Int + d : Optional Int + deriving (Eq, Show) + +instance Hashable VarV2 where + hash v = + case v of + V1V2 with payload; c; d -> + hashUpgradedVariant + "V1" + [ hash payload.x + , hash payload.y + ] + [ fmap hash c + , fmap hash d + ] + + V2V2 with payload; c; d -> + hashUpgradedVariant + "V2" + [ hash payload.x + , hash payload.y + ] + [ fmap hash c + , fmap hash d + ] + + +-------------------------------------------------------------------------------- +-- Structural Behavior Tests +-------------------------------------------------------------------------------- + +test_emptyListDifferent : Script () +test_emptyListDifferent = script do + hash ([] : [Int]) =/= hash ([1] : [Int]) + +test_listLengthMatters : Script () +test_listLengthMatters = script do + hash ([1,2] : [Int]) =/= hash ([1,2,3] : [Int]) + + +test_listStructureMatters : Script () +test_listStructureMatters = script do + hash ([[1],[2]] : [[Int]]) =/= hash ([1,2] : [Int]) + + +test_recordFieldCountMatters : Script () +test_recordFieldCountMatters = script do + let r = RecV1 with a = 1; b = "x" + let r' = RecV1' with a = 1 + hash r =/= hash r' + + +-------------------------------------------------------------------------------- +-- Variant Separation +-------------------------------------------------------------------------------- + +test_variantTagMatters : Script () +test_variantTagMatters = script do + let payload = Payload with x = 1; y = "x" + let v1 = V1 with payload + let v2 = V2 with payload + hash v1 =/= hash v2 + + +test_variantValueMatters : Script () +test_variantValueMatters = script do + let p1 = Payload with x = 1; y = "x" + let p2 = Payload with x = 2; y = "x" + let v1 = V1 with payload = p1 + let v2 = V1 with payload = p2 + hash v1 =/= hash v2 + + +-------------------------------------------------------------------------------- +-- Optional Semantics +-------------------------------------------------------------------------------- + +test_optionalNoneVsSome : Script () +test_optionalNoneVsSome = script do + hash (None : Optional Int) =/= hash (Some 1) + + +-------------------------------------------------------------------------------- +-- Record Upgrade Compatibility (Multiple Optionals) +-------------------------------------------------------------------------------- + +test_recordUpgrade_allUnset : Script () +test_recordUpgrade_allUnset = script do + let r1 = RecV1 with a = 1; b = "abc" + let r2 = + RecV3 + with + a = 1 + b = "abc" + c = None + d = None + hash r1 === hash r2 + + +test_recordUpgrade_firstSet : Script () +test_recordUpgrade_firstSet = script do + let r1 = RecV1 with a = 1; b = "abc" + let r2 = + RecV3 + with + a = 1 + b = "abc" + c = Some 1 + d = None + hash r1 =/= hash r2 + + +test_recordUpgrade_secondSet : Script () +test_recordUpgrade_secondSet = script do + let r1 = RecV1 with a = 1; b = "abc" + let r2 = + RecV3 + with + a = 1 + b = "abc" + c = None + d = Some 1 + hash r1 =/= hash r2 + + +test_recordUpgrade_bothSet : Script () +test_recordUpgrade_bothSet = script do + let r1 = RecV1 with a = 1; b = "abc" + let r2 = + RecV3 + with + a = 1 + b = "abc" + c = Some 1 + d = Some 1 + hash r1 =/= hash r2 + +test_recordUpgradeOrderOfOptionalsDoesNotMatter : Script () +test_recordUpgradeOrderOfOptionalsDoesNotMatter = script do + let r1' = RecV2 with + a = 1 + b = "abc" + c = Some 1 + let r1 = RecV3 with + a = 1 + b = "abc" + c = Some 1 + d = None + let r2 = RecV3 with + a = 1 + b = "abc" + c = None + d = Some 1 + hash r1 === hash r1' + hash r1 =/= hash r2 + + +-------------------------------------------------------------------------------- +-- Variant Upgrade Compatibility (Multiple Optionals) +-------------------------------------------------------------------------------- + +test_variantUpgrade_allUnset : Script () +test_variantUpgrade_allUnset = script do + let payload = Payload with x = 5; y = "x" + + let v1 = V1 with payload + + let v2 = + V1V2 + with + payload + c = None + d = None + + assertEq (hash v1) (hash v2) + + +test_variantUpgrade_firstSet : Script () +test_variantUpgrade_firstSet = script do + let payload = Payload with x = 5; y = "x" + + let v1 = V1 with payload + + let v2 = + V1V2 + with + payload + c = Some 9 + d = None + + hash v1 =/= hash v2 + + +test_variantUpgrade_secondSet : Script () +test_variantUpgrade_secondSet = script do + let payload = Payload with x = 5; y = "x" + + let v1 = V1 with payload + + let v2 = + V1V2 + with + payload + c = None + d = Some 1 + + hash v1 =/= hash v2 + + +test_variantUpgrade_bothSet : Script () +test_variantUpgrade_bothSet = script do + let payload = Payload with x = 5; y = "x" + + let v1 = V1 with payload + + let v2 = + V1V2 + with + payload + c = Some 1 + d = Some 1 + + hash v1 =/= hash v2 + + +test_variantUpgrade_constructorSeparationStillHolds : Script () +test_variantUpgrade_constructorSeparationStillHolds = script do + let payload = Payload with x = 5; y = "x" + + let v1 = V1V2 with payload; c = None; d = None + let v2 = V2V2 with payload; c = None; d = None + + hash v1 =/= hash v2 + + +-------------------------------------------------------------------------------- +-- Golden Structural Stability +-------------------------------------------------------------------------------- + +test_golden_recordEncoding : Script () +test_golden_recordEncoding = script do + let r = RecV1 with a = 1; b = "x" + + let expected = "e2878cf11d8a10aa17f359e1f61f711756fdbbc256bf541baec14c21b6888f6e" + + assertEq (hash r).value expected + + +test_golden_variantEncoding : Script () +test_golden_variantEncoding = script do + let payload = Payload with x = 1; y = "x" + let v = V1 with payload + + let expected = "ca314e0bdf0fc327940a89334ff6df58f234b395d36328af1a0cce2339227e5c" + + assertEq (hash v).value expected diff --git a/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml b/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml index 82a2c5f36e..b22fc277ab 100644 --- a/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml +++ b/daml/splice-amulet-test/daml/Splice/Scripts/Util.daml @@ -47,8 +47,17 @@ import Splice.Util data AmuletApp = AmuletApp with dso : Party dsoUser : AmuletUser + registry : Registry.AmuletRegistry deriving (Eq, Ord, Show) +mkAmuletApp : Party -> AmuletUser -> AmuletApp +mkAmuletApp dso dsoUser = AmuletApp with + dso + dsoUser + registry = Registry.AmuletRegistry with + dso + instrumentId = amuletInstrumentId dso + demoTime : Time demoTime = time (DA.Date.date 2022 Jan 1) 0 0 0 @@ -57,21 +66,30 @@ demoTime = time (DA.Date.date 2022 Jan 1) 0 0 0 setupApp : Script AmuletApp setupApp = genericSetupApp "" +setupAppWithConfig : AmuletConfig Unit.USD -> Script AmuletApp +setupAppWithConfig config = genericSetupAppWithConfig config "" + -- | Setup the DSO party with a specific prefix and the contracts defining the Amulet app. genericSetupApp : Text -> Script AmuletApp -genericSetupApp dsoPrefix = do +genericSetupApp dsoPrefix = genericSetupAppWithConfig defaultAmuletConfig dsoPrefix + +-- | Setup the DSO party with a specific prefix and the contracts defining the Amulet app. +genericSetupAppWithConfig : AmuletConfig Unit.USD -> Text -> Script AmuletApp +genericSetupAppWithConfig config dsoPrefix = do -- use a time that is easy to reason about in script outputs setTime demoTime dso <- allocateParty (dsoPrefix <> "dso-party") dsoUser <- validateUserId (dsoPrefix <> "dummy-dso-user") _ <- createUser (User dsoUser (Some dso)) [] - let app = AmuletApp with dso, dsoUser = AmuletUser dsoUser dso + let app = mkAmuletApp dso (AmuletUser dsoUser dso) recordValidatorOf app app.dso app.dso _ <- submit dso $ createCmd AmuletRules with - configSchedule = defaultAmuletConfigSchedule + configSchedule = Schedule with + initialValue = config + futureValues = [] isDevNet = True contractStateSchemaVersion = None .. @@ -87,6 +105,7 @@ genericSetupApp dsoPrefix = do return app + -- Replacing AmuletConfig -------------------------- @@ -219,11 +238,17 @@ runNextIssuance : AmuletApp -> Script () runNextIssuance app = do runNextIssuanceInternal app 1.0 +-- | Run the next issuance with an off-ledger calculated app activity round total (in MB of traffic). +-- Used when the round uses traffic-based app rewards. +runNextIssuanceWithTrafficBasedAppRewards : AmuletApp -> Decimal -> Script () +runNextIssuanceWithTrafficBasedAppRewards app appActivityRoundTotal = do + Registry.runNextIssuance app.dso 1.0 (Some appActivityRoundTotal) + -- Only directly call this function if you know what you are doing. -- Due to effective-dating, the price you give here is not the amulet price that can be used immediately runNextIssuanceInternal : AmuletApp -> Decimal -> Script () runNextIssuanceInternal app amuletPrice = do - Registry.runNextIssuance app.dso amuletPrice + Registry.runNextIssuance app.dso amuletPrice None -- Set time so that at least one mining round is open and usable passTimeToRoundOpen : AmuletApp -> Script () @@ -312,7 +337,7 @@ pay : AmuletApp -> AmuletUser -> Party -> Decimal -> Script () pay app sender recipient amuletAmount = do payWithTransferFeeRatio app sender recipient amuletAmount 0.0 --- | A simple and slightly hacky-way to test payment: it just gathers all amulets from +-- | A simple and slightly hacky-way to test payment: it just gathers all amulets and RewarCouponV2s from -- the sender and transfers the desired amount off of them to the receiver. -- The left-over amount is kept. payWithTransferFeeRatio : AmuletApp -> AmuletUser -> Party -> Decimal -> Decimal -> Script () @@ -320,10 +345,11 @@ payWithTransferFeeRatio app sender recipient amuletAmount transferFeeRatio = do -- TODO(tech-debt): create payment pre-approval flow and use that here instead of submitMulti readAs <- getUserReadAs sender.userId amulets <- getAmuletInputs sender.primaryParty + rewardCoupons <- query @RewardCouponV2 sender.primaryParty let transfer = Transfer with sender = sender.primaryParty provider = sender.primaryParty - inputs = amulets + inputs = amulets ++ map (InputRewardCouponV2 . fst) rewardCoupons outputs = [ TransferOutput with receiver = recipient @@ -415,6 +441,9 @@ runAmuletDepositBots app = do svRewardCoupons <- queryFilter @SvRewardCoupon app.dso $ \c -> c.round `elem` issuingRoundNumbers && c.beneficiary == user + rewardCouponV2s <- queryFilter @RewardCouponV2 app.dso $ \c -> + fromOptional c.provider c.beneficiary == user + -- get all amulets of this user amulets <- getAmuletInputs user -- Need readAs rights for all hosted users to collect their validator rewards, @@ -431,6 +460,7 @@ runAmuletDepositBots app = do map (mkInputValidatorFaucetCoupon . fst) validatorFaucetCoupons ++ map (InputValidatorLivenessActivityRecord . fst) validatorLivenessActivityRecords ++ map (InputSvRewardCoupon . fst) svRewardCoupons ++ + map (InputRewardCouponV2 . fst) rewardCouponV2s ++ amulets outputs = [] beneficiaries = None -- no beneficiaries for self-transfer @@ -447,6 +477,28 @@ runAmuletDepositBots app = do require "Owners of amulets must be unique" (unique amuletOwners) pure () +mintRewardsV2 : AmuletApp -> Party -> Script () +mintRewardsV2 app beneficiary = do + -- create transfer context + (openMiningRound, _) <- getLatestOpenRound app + let context = TransferContext with + openMiningRound + issuingMiningRounds = Map.empty + validatorRights = Map.empty + featuredAppRight = None + -- query the coupons + coupons <- query @RewardCouponV2 beneficiary + void $ submitExerciseAmuletRulesByKey app [beneficiary] [] AmuletRules_Transfer with + transfer = Transfer with + sender = beneficiary + provider = beneficiary + inputs = map (InputRewardCouponV2 . fst) coupons + outputs = [] + beneficiaries = None -- no featured-app-marker beneficiaries + context + expectedDso = Some app.dso + + -- | Retrieve the list of all amulets that the given party can use as transfer inputs. getAmuletInputs : Party -> Script [TransferInput] getAmuletInputs sender = do @@ -624,6 +676,12 @@ setAmuletConfig app baseConfig newConfig = do newConfig baseConfig +updateAmuletConfig : AmuletApp -> (AmuletConfig Unit.USD -> AmuletConfig Unit.USD) -> Script () +updateAmuletConfig app f = do + baseConfig <- getAmuletConfig app + let newConfig = f baseConfig + setAmuletConfig app baseConfig newConfig + allocateDevelopmentFundCoupon : AmuletApp -> Party -> Party -> Decimal -> Time -> Text -> [ContractId UnclaimedDevelopmentFundCoupon] -> Script AmuletRules_AllocateDevelopmentFundCouponResult diff --git a/daml/splice-amulet/daml.yaml b/daml/splice-amulet/daml.yaml index bf5ebbe4a2..d4530d70d0 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.17 +version: 0.1.18 dependencies: - daml-prim - daml-stdlib @@ -19,6 +19,7 @@ data-dependencies: - ../splice-util/.daml/dist/splice-util-current.dar - ../splice-api-featured-app-v1/.daml/dist/splice-api-featured-app-v1-current.dar - ../splice-api-featured-app-v2/.daml/dist/splice-api-featured-app-v2-current.dar + - ../splice-api-reward-assignment-v1/.daml/dist/splice-api-reward-assignment-v1-current.dar build-options: - --ghc-option=-Wunused-binds - --ghc-option=-Wunused-matches diff --git a/daml/splice-amulet/daml/Splice/Amulet.daml b/daml/splice-amulet/daml/Splice/Amulet.daml index 14481a9486..d683cf2ee9 100644 --- a/daml/splice-amulet/daml/Splice/Amulet.daml +++ b/daml/splice-amulet/daml/Splice/Amulet.daml @@ -7,12 +7,14 @@ module Splice.Amulet where import Prelude import DA.Action (void) import DA.Assert +import DA.List (unique) import DA.Map as Map import DA.TextMap as TextMap -import DA.Optional (fromOptional) +import DA.Optional (fromOptional, isNone, optionalToList) import Splice.Api.Token.MetadataV1 qualified as Api.Token.MetadataV1 import Splice.Api.Token.HoldingV1 qualified as Api.Token.HoldingV1 +import Splice.Api.RewardAssignmentV1 qualified as Api.RewardAssignmentV1 import Splice.Amulet.TokenApiUtils import Splice.Expiry @@ -267,38 +269,37 @@ template FeaturedAppRight with controller provider do return FeaturedAppRight_CancelResult - interface instance Splice.Api.FeaturedAppRightV1.FeaturedAppRight for FeaturedAppRight where view = Splice.Api.FeaturedAppRightV1.FeaturedAppRightView with dso, provider featuredAppRight_CreateActivityMarkerImpl _self arg = do - validateAppRewardBeneficiaries arg.beneficiaries - let groupedBeneficiaries = Map.fromListWithR (+) (map (\b -> (b.beneficiary, b.weight)) arg.beneficiaries) - cids <- forA (Map.toList groupedBeneficiaries) $ \(beneficiary, weight) -> - create FeaturedAppActivityMarker - with - dso - provider - beneficiary = beneficiary - weight = weight - pure (Splice.Api.FeaturedAppRightV1.FeaturedAppRight_CreateActivityMarkerResult $ map toInterfaceContractId cids) + validateAppRewardBeneficiaries arg.beneficiaries + let groupedBeneficiaries = Map.fromListWithR (+) (map (\b -> (b.beneficiary, b.weight)) arg.beneficiaries) + cids <- forA (Map.toList groupedBeneficiaries) $ \(beneficiary, weight) -> + create FeaturedAppActivityMarker + with + dso + provider + beneficiary = beneficiary + weight = weight + pure (Splice.Api.FeaturedAppRightV1.FeaturedAppRight_CreateActivityMarkerResult $ map toInterfaceContractId cids) interface instance Splice.Api.FeaturedAppRightV2.FeaturedAppRight for FeaturedAppRight where view = Splice.Api.FeaturedAppRightV2.FeaturedAppRightView with dso, provider featuredAppRight_CreateActivityMarkerImpl _self arg = do - validateAppRewardBeneficiariesV2 arg.beneficiaries - require "Weight >= 1.0" (fromOptional 1.0 arg.weight >= 1.0) - require "Weight <= 10000.0" (fromOptional 1.0 arg.weight <= 10000.0) - let groupedBeneficiaries = Map.fromListWithR (+) (map (\b -> (b.beneficiary, b.weight)) arg.beneficiaries) - cids <- forA (Map.toList groupedBeneficiaries) $ \(beneficiary, weight) -> - create FeaturedAppActivityMarker - with - dso - provider - beneficiary = beneficiary - weight = weight * fromOptional 1.0 arg.weight - pure (Splice.Api.FeaturedAppRightV2.FeaturedAppRight_CreateActivityMarkerResult $ map toInterfaceContractId cids) + validateAppRewardBeneficiariesV2 arg.beneficiaries + require "Weight >= 1.0" (fromOptional 1.0 arg.weight >= 1.0) + require "Weight <= 10000.0" (fromOptional 1.0 arg.weight <= 10000.0) + let groupedBeneficiaries = Map.fromListWithR (+) (map (\b -> (b.beneficiary, b.weight)) arg.beneficiaries) + cids <- forA (Map.toList groupedBeneficiaries) $ \(beneficiary, weight) -> + create FeaturedAppActivityMarker + with + dso + provider + beneficiary = beneficiary + weight = weight * fromOptional 1.0 arg.weight + pure (Splice.Api.FeaturedAppRightV2.FeaturedAppRight_CreateActivityMarkerResult $ map toInterfaceContractId cids) validateAppRewardBeneficiaries : [Splice.Api.FeaturedAppRightV1.AppRewardBeneficiary] -> Update () validateAppRewardBeneficiaries beneficiaries = do @@ -372,6 +373,44 @@ template AppRewardCoupon return AppRewardCoupon_DsoExpireResult with .. +-- | A coupon for receiving rewards, which is currently only used for +-- traffic-based app rewards. +-- +-- Designed to be flexible so we could extend it to include other kinds of rewards +-- that are based on on off-ledger calculations. +-- See Splice.Amulet.RewardAccountingV2 for details. +template RewardCouponV2 + with + dso : Party + provider : Party -- ^ The party that provided the service for whose activity the minting right was granted. + round : Round -- ^ The round in which the providers activity was recorded. + amount : Decimal -- ^ The amount of reward that can be minted for this coupon. Denominated in Amulet. + expiresAt : Time -- ^ Time-based expiry. Used to determine when the reward can no longer be minted. + providerIsObserver : Bool + -- ^ Whether the provider should be an observer, which they are unless + -- their vetting state at the time of coupon creation does not allow it. + -- DSO automation will then attempt to make the provider an observer when they + -- change their vetting state unless the coupon expired in the meantime. + beneficiary : Optional Party + -- ^ The party that can mint the reward for the activity by the provider. + -- If not set, this is the provider. + where + signatory dso + observer if providerIsObserver then [provider] ++ optionalToList beneficiary else [] + ensure amount > 0.0 + + interface instance Api.RewardAssignmentV1.RewardCoupon for RewardCouponV2 where + view = Api.RewardAssignmentV1.RewardCouponView with + dso + provider + beneficiary = fromOptional provider beneficiary + amount + expiresAt + meta = Api.Token.MetadataV1.emptyMetadata + maxNumNewBeneficiaries = maxNumNewCouponV2Beneficiaries + + rewardCoupon_assignBeneficiariesImpl _self arg = rewardCouponV2_assignBeneficiariesImpl this arg + -- | A coupon for receiving validator rewards proportional to the usage fee paid by a user -- hosted by a validator operator. @@ -537,6 +576,9 @@ template UnclaimedActivityRecord pure UnclaimedActivityRecord_DsoExpireResult with unclaimedRewardCid +-- Support code +--------------- + requireAmuletExpiredForAllRounds : ContractId ExternalPartyConfigState -> ContractId ExternalPartyConfigState -> Amulet -> Update () requireAmuletExpiredForAllRounds externalPartyConfigState0Cid externalPartyConfigState1Cid amulet = do require "externalPartyConfigState0Cid /= externalPartyConfigState1Cid" (externalPartyConfigState0Cid /= externalPartyConfigState1Cid) @@ -546,6 +588,47 @@ requireAmuletExpiredForAllRounds externalPartyConfigState0Cid externalPartyConfi require "Amulet is expired" (isAmuletExpired round amulet.amount) +-- | Maximum number of new beneficiaries that can be assigned to a RewardCouponV2. +-- Set to the same limit as the one for featured-app-marker beneficiaries to minimize migration effort +-- for app providers. +maxNumNewCouponV2Beneficiaries : Int +maxNumNewCouponV2Beneficiaries = 20 + +-- | Implementation of the 'RewardCoupon_AssignBeneficiaries' choice for 'RewardCouponV2'. +rewardCouponV2_assignBeneficiariesImpl + : RewardCouponV2 + -> Api.RewardAssignmentV1.RewardCoupon_AssignBeneficiaries + -> Update Api.RewardAssignmentV1.RewardCoupon_AssignBeneficiariesResult +rewardCouponV2_assignBeneficiariesImpl coupon arg = do + let newBeneficiaries = arg.newBeneficiaries + require "Beneficiary has not been assigned yet" (isNone coupon.beneficiary) + require "Beneficaries are unique" (unique (map (.beneficiary) newBeneficiaries)) + require "Beneficiary weights are between 0.0 and 1.0" + (all (\b -> 0.0 < b.weight && b.weight <= 1.0) newBeneficiaries) + require "Beneficiary weights add up to 1.0" (sum (map (.weight) newBeneficiaries) == 1.0) + require ("No more than " <> show maxNumNewCouponV2Beneficiaries <> " beneficiaries assigned") + (length newBeneficiaries <= maxNumNewCouponV2Beneficiaries) + + additionalCoupons <- forA arg.additionalCoupons $ \cid -> do + c <- fetchAndArchive (ForOwner with dso = coupon.dso; owner = coupon.provider) (fromInterfaceContractId @RewardCouponV2 cid) + require "Additional coupon has not been assigned yet" (isNone c.beneficiary) + require "Additional coupon has same provider" (c.provider == coupon.provider) + pure c + + let allCoupons = coupon :: additionalCoupons + + -- create coupons for new beneficiaries for each coupon + perBeneficiaryCids <- forA newBeneficiaries $ \newBeneficiary -> do + cids <- forA allCoupons $ \c -> + toInterfaceContractId <$> create c with + amount = c.amount * newBeneficiary.weight + beneficiary = Some newBeneficiary.beneficiary + providerIsObserver = True + pure (newBeneficiary.beneficiary, cids) + + return Api.RewardAssignmentV1.RewardCoupon_AssignBeneficiariesResult with + newBeneficiariesCouponCids = perBeneficiaryCids + -- instances ------------ @@ -559,6 +642,12 @@ instance HasCheckedFetch LockedAmulet ForOwner where instance HasCheckedFetch AppRewardCoupon ForOwner where contractGroupId AppRewardCoupon{..} = ForOwner with dso; owner = fromOptional provider beneficiary +instance HasCheckedFetch RewardCouponV2 ForOwner where + contractGroupId RewardCouponV2{..} = ForOwner with dso; owner = fromOptional provider beneficiary + +instance HasCheckedFetch RewardCouponV2 ForDso where + contractGroupId RewardCouponV2{..} = ForDso with dso + instance HasCheckedFetch SvRewardCoupon ForOwner where contractGroupId SvRewardCoupon{..} = ForOwner with dso; owner = beneficiary diff --git a/daml/splice-amulet/daml/Splice/Amulet/CryptoHash.daml b/daml/splice-amulet/daml/Splice/Amulet/CryptoHash.daml new file mode 100644 index 0000000000..951b096c22 --- /dev/null +++ b/daml/splice-amulet/daml/Splice/Amulet/CryptoHash.daml @@ -0,0 +1,116 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +-- | Utilities to compute cryptographic hashes of Daml data structures. +-- We use this for computing compact commitments for reflecting off-ledger data +-- shared by the SV nodes on-ledger. +-- +-- Note that the hashes are based on viewing all scalar values as strings and taking +-- a structural view of Daml records; i.e., the hashes do not include a unique type +-- tag by default. Make sure to include a type tag using `hashVariant` if you +-- want to hash different data structures in the same scope. +module Splice.Amulet.CryptoHash + ( + Hash(..), + Hashable(..), + hashRecord, + hashUpgradedRecord, + hashVariant, + hashUpgradedVariant, + ) where + +import DA.Optional (isNone) +import DA.Text qualified as T + +data Hash = Hash with value : Text + deriving (Eq, Show) + +-- | Compute the hash of a record. +hashRecord : [Hash] -> Hash +hashRecord = hashListInternal . map (.value) + +-- | Compute the hash of an upgraded record so that it agrees with the old record hash +-- when ignoring trailing None fields. +hashUpgradedRecord : [Hash] -> [Optional Hash] -> Hash +hashUpgradedRecord oldFieldHashes newFieldHashes = + hashListInternal $ + [ h.value | h <- oldFieldHashes ] ++ + [ (hashOptionalInternal optField).value | optField <- dropTrailingNones newFieldHashes ] + +-- | Compute the hash of a variant. +hashVariant : Text -> [Hash] -> Hash +hashVariant tag fieldHashes = hashVariantInternal tag [ h.value | h <- fieldHashes ] + +-- | Compute the hash of an upgraded variant so that it agrees with the old variant hash +-- when ignoring trailing None fields. +hashUpgradedVariant : Text -> [Hash] -> [Optional Hash] -> Hash +hashUpgradedVariant tag oldFieldHashes newFieldHashes = + hashVariantInternal tag $ + [ h.value | h <- oldFieldHashes ] ++ + [ (hashOptionalInternal optField).value | optField <- dropTrailingNones newFieldHashes ] + +class Hashable a where + hash : a -> Hash + +-- | Identity instance for Hash, which is useful for hash types like [Hash]. +instance Hashable Hash where + hash h = h + +instance Hashable Int where + hash = hashText . show + +instance Hashable Decimal where + hash = hashText . show + +instance Hashable Party where + hash = hashText . partyToText + +instance Hashable Text where + hash = hashText + +instance Hashable a => Hashable (Optional a) where + hash = hashOptionalInternal . fmap hash + +instance Hashable a => Hashable [a] where + hash = hashList hash + + +-- internal helper functions +---------------------------- + +-- Design Note: we want these hashes to be easy to compute in many systems. +-- Therefore we essentially encode the data structure as an S-expression and hash that +-- one recursively. Concretely, we use the following rules: +-- +-- - hash scalars by hashing their string rendering +-- - hash lists by hashing the length and the element hashes using "|" as a separator +-- - hash records by hashing the list of field hashes +-- - hash variants by hashing the list of fields prefixed with tag for the variant constructor +-- +-- The length prefix on lists also serves as a tag to distinguish different tree structures. +-- We include the number of fields in the hash of a record, as the number of fields +-- can change as part of a Smart Contract Upgrades. +-- +-- Tags for variants must be unique within the scope where the hashes are used. + + +hashList : (a -> Hash) -> [a] -> Hash +hashList hashElem xs = hashListInternal [ (hashElem x).value | x <- xs ] + +hashText : Text -> Hash +hashText = Hash . T.sha256 + +hashListInternal : [Text] -> Hash +hashListInternal ts = Hash $ T.sha256 $ T.intercalate "|" (show (length ts) :: ts) + +hashVariantInternal : Text -> [Text] -> Hash +hashVariantInternal tag fieldValues = + Hash $ T.sha256 $ T.intercalate "|" (tag :: show (length fieldValues) :: fieldValues) + +-- we view optionals as lists of length 0 or 1 to simplify the encoding in other systems +hashOptionalInternal : Optional Hash -> Hash +hashOptionalInternal None = hashListInternal [] +hashOptionalInternal (Some h) = hashListInternal [h.value] + +dropTrailingNones : [Optional Hash] -> [Optional Hash] +dropTrailingNones = reverse . dropWhile isNone . reverse diff --git a/daml/splice-amulet/daml/Splice/Amulet/RewardAccountingV2.daml b/daml/splice-amulet/daml/Splice/Amulet/RewardAccountingV2.daml new file mode 100644 index 0000000000..53993578dd --- /dev/null +++ b/daml/splice-amulet/daml/Splice/Amulet/RewardAccountingV2.daml @@ -0,0 +1,129 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +-- | Templates and support code for reward accounting based on off-ledger calculations, as initially +-- described in CIP-104 for traffic-based app rewards. +-- +-- It is a Version 2 compared to the previous reward accounting mechanism that is based on tracking +-- all state on-ledger using App|Validator|SvRewardCoupon and IssuingMiningRound contracts. +-- +-- The design is flexible to allow for extending it to other kinds of rewards +-- that are based on off-ledger calculations, but the initial implementation is +-- specialized to traffic-based app rewards. +module Splice.Amulet.RewardAccountingV2 where + +import DA.Action (unless) +import DA.Foldable (forA_) +import DA.Set as Set +import DA.Time + +import Splice.Amulet.CryptoHash qualified as CryptoHash +import Splice.Amulet +import Splice.Types +import Splice.Util + +-- | State contract tracking the need to calculate and confirm the app reward amounts for the given round. +template CalculateRewardsV2 with + dso : Party + round : Round + rewardCouponTimeToLive : RelTime + -- ^ Time to live for reward coupons that are created. + -- + -- We store this as part of requesting the calculation of rewards to avoid reading + -- the AmuletConfig later again, which could lead to inconsistencies in case of config changes. + dryRun : Bool + -- ^ Whether to only simulate the confirmation without creating any RewardCouponV2 contracts. + where + signatory dso + +-- | A minting allowance for a service provider to mint Amulet. +data MintingAllowance = MintingAllowance with + provider : Party + amount : Decimal + deriving (Eq, Show) + +type MintingAllowances = [MintingAllowance] + +data Batch + = BatchOfBatches [CryptoHash.Hash] + | BatchOfMintingAllowances MintingAllowances + deriving (Eq, Show) + +data ProcessRewardsV2_ProcessBatchResult = ProcessRewardsV2_ProcessBatchResult {} + deriving (Eq, Show) + +-- | State contract tracking outstanding processing of rewards for a given round and batch hash. +template ProcessRewardsV2 with + dso : Party + round : Round + dryRun : Bool -- ^ Whether to only simulate the processing without creating any RewardCouponV2 contracts. + rewardCouponTimeToLive : RelTime + batchHash : CryptoHash.Hash + where + signatory dso + + choice ProcessRewardsV2_ProcessBatch : ProcessRewardsV2_ProcessBatchResult + with + batch : Batch + providersWithWrongVettingState : Set Party + -- ^ Service providers that do not have the correct vetting state for receiving rewards. + observer batchProviders providersWithWrongVettingState batch + controller dso + do + let actualHash = CryptoHash.hash batch + require "batch hash matches" (actualHash == batchHash) + case batch of + BatchOfBatches batchHashes -> do + forA_ batchHashes $ \newBatchHash -> + create ProcessRewardsV2 with + dso + round + rewardCouponTimeToLive + dryRun + batchHash = newBatchHash + BatchOfMintingAllowances mintingAllowances -> do + unless dryRun $ do + -- Coupon expiry is determined here based on the time of creation to ensure + -- providers are always given the full rewardCouponTimeToLive duration to redeem their coupons, + -- independent of how long the processing takes and how many batches there are. + now <- getTime + let expiresAt = now `addRelTime` rewardCouponTimeToLive + forA_ mintingAllowances $ \MintingAllowance{..} -> + create RewardCouponV2 with + dso + round + provider + amount + expiresAt + providerIsObserver = + not $ Set.member provider providersWithWrongVettingState + beneficiary = None + + -- intentionally not returning any information here to save computational overhead + return ProcessRewardsV2_ProcessBatchResult {} + +batchProviders : Set Party -> Batch -> [Party] +batchProviders ignoredProviders batch = case batch of + BatchOfBatches _ -> [] + BatchOfMintingAllowances mintingAllowances -> + [ provider | MintingAllowance{..} <- mintingAllowances, not (Set.member provider ignoredProviders) ] + + +-- instances + +instance CryptoHash.Hashable MintingAllowance where + hash MintingAllowance {provider, amount} = + CryptoHash.hashRecord [CryptoHash.hash provider , CryptoHash.hash amount] + +instance CryptoHash.Hashable Batch where + hash batch = case batch of + BatchOfBatches batchHashes -> + CryptoHash.hashVariant "BatchOfBatches" [CryptoHash.hash batchHashes] + BatchOfMintingAllowances mintingAllowances -> + CryptoHash.hashVariant "BatchOfMintingAllowances" [CryptoHash.hash mintingAllowances] + +instance HasCheckedFetch CalculateRewardsV2 ForDso where + contractGroupId CalculateRewardsV2 with .. = ForDso with .. + +instance HasCheckedFetch ProcessRewardsV2 ForDso where + contractGroupId ProcessRewardsV2 with .. = ForDso with .. diff --git a/daml/splice-amulet/daml/Splice/AmuletConfig.daml b/daml/splice-amulet/daml/Splice/AmuletConfig.daml index 76b80c1a4c..728915188c 100644 --- a/daml/splice-amulet/daml/Splice/AmuletConfig.daml +++ b/daml/splice-amulet/daml/Splice/AmuletConfig.daml @@ -47,6 +47,42 @@ data TransferConfigV2 unit = TransferConfigV2 with maxNumLockHolders : Int deriving (Eq, Show) + +-- Reward configuration +----------------------- + +-- | How rewards and their minting allowances are tracked and computed. +data RewardVersion + = RewardVersion_FeaturedAppMarkers + -- ^ Version of app rewards pre CIP-104 + | RewardVersion_TrafficBasedAppRewards + -- ^ Traffic-based app rewards as introduced in CIP-104 + deriving (Eq, Show) + +useTrafficBasedAppRewards : Optional RewardVersion -> Bool +useTrafficBasedAppRewards rv = case rv of + None -> False + Some RewardVersion_FeaturedAppMarkers -> False + Some RewardVersion_TrafficBasedAppRewards -> True + +useFeaturedAppMarkers : Optional RewardVersion -> Bool +useFeaturedAppMarkers = not . useTrafficBasedAppRewards + +-- | Configuration for how reward accounting and calculation should be performed. +data RewardConfig = RewardConfig with + mintingVersion : RewardVersion -- ^ What scheme to use for minting rewards. + dryRunVersion : Optional RewardVersion -- ^ What scheme to use for dry-running reward minting. If None, dry-runs are disabled. + batchSize : Int -- ^ Batch size to use for building the Merkle-tree over minting allowances. + rewardCouponTimeToLive : RelTime -- ^ Time to live for reward coupons, default 36h to support batching of collection across 12h with a 24h prepare-submission delay. + appRewardCouponThreshold : Decimal -- ^ Threshold for minting reward coupons, in USD. + -- This threshold is not enforced in daml, but it must be used while building the Merkle-tree + -- to ignore minting allowances lower than this value. + deriving (Eq, Show) + + +-- Full AmuletConfig +-------------------- + -- | Configuration includes TransferConfig, issuance curve and tickDuration -- -- See Splice.Scripts.Parameters for concrete values. @@ -65,6 +101,8 @@ data AmuletConfig unit = AmuletConfig with -- ^ Party authorized to manage and allocate minting rights from the Development Fund. externalPartyConfigStateTickDuration : Optional RelTime -- ^ Half the lifetime of an `ExternalPartyConfigState` contract and the overlap between two successive contracts. Default: 24h. + rewardConfig : Optional RewardConfig + -- ^ Configuration for the reward accounting and calculation. If None, then on-ledger reward tracking is used. deriving (Eq, Show) getExternalPartyConfigStateTickDuration : AmuletConfig a -> RelTime @@ -138,6 +176,9 @@ data PackageConfig = PackageConfig validPackageConfig : PackageConfig -> Bool validPackageConfig _ = True +instance Patchable RewardVersion where + patch = patchScalar + instance Patchable (AmuletConfig USD) where patch new base current = AmuletConfig with transferConfig = patch new.transferConfig base.transferConfig current.transferConfig @@ -149,6 +190,15 @@ instance Patchable (AmuletConfig USD) where featuredAppActivityMarkerAmount = patch new.featuredAppActivityMarkerAmount base.featuredAppActivityMarkerAmount current.featuredAppActivityMarkerAmount optDevelopmentFundManager = patch new.optDevelopmentFundManager base.optDevelopmentFundManager current.optDevelopmentFundManager externalPartyConfigStateTickDuration = patch new.externalPartyConfigStateTickDuration base.externalPartyConfigStateTickDuration current.externalPartyConfigStateTickDuration + rewardConfig = patch new.rewardConfig base.rewardConfig current.rewardConfig + +instance Patchable RewardConfig where + patch new base current = RewardConfig with + mintingVersion = patch new.mintingVersion base.mintingVersion current.mintingVersion + dryRunVersion = patch new.dryRunVersion base.dryRunVersion current.dryRunVersion + batchSize = patch new.batchSize base.batchSize current.batchSize + rewardCouponTimeToLive = patch new.rewardCouponTimeToLive base.rewardCouponTimeToLive current.rewardCouponTimeToLive + appRewardCouponThreshold = patch new.appRewardCouponThreshold base.appRewardCouponThreshold current.appRewardCouponThreshold instance Patchable (TransferConfig USD) where patch new base current = TransferConfig with diff --git a/daml/splice-amulet/daml/Splice/AmuletRules.daml b/daml/splice-amulet/daml/Splice/AmuletRules.daml index f903659606..5b9899a04a 100644 --- a/daml/splice-amulet/daml/Splice/AmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/AmuletRules.daml @@ -10,10 +10,10 @@ import DA.Action (filterA, foldlA, when, unless, void) import DA.Assert import DA.Exception import DA.Foldable (forA_) -import DA.List (dedupSort, maximumOn) +import DA.List (sort, dedupSort, maximumOn) import DA.Map (Map) import qualified DA.Map as Map -import DA.Optional (fromOptional, isNone, optionalToList) +import DA.Optional (fromOptional, isNone, isSome, optionalToList) import DA.Set (Set) import qualified DA.Set as Set import qualified DA.TextMap as TextMap @@ -25,8 +25,14 @@ import Splice.Api.FeaturedAppRightV1 (AppRewardBeneficiary(..)) import Splice.Api.Token.MetadataV1 as Api.Token.MetadataV1 import Splice.Api.Token.HoldingV1 qualified as Api.Token.HoldingV1 import Splice.Amulet +import Splice.Amulet.CryptoHash qualified as CryptoHash +import Splice.Amulet.RewardAccountingV2 import Splice.Amulet.TokenApiUtils -import Splice.AmuletConfig (transferConfigToTransferConfigV2, AmuletConfig(..), TransferConfig(..), TransferConfigV2(..), validAmuletConfig, defaultTransferPreapprovalFee, getExternalPartyConfigStateTickDuration) +import Splice.AmuletConfig + ( transferConfigToTransferConfigV2, AmuletConfig(..), TransferConfig(..), TransferConfigV2(..), validAmuletConfig + , defaultTransferPreapprovalFee, getExternalPartyConfigStateTickDuration, useTrafficBasedAppRewards + , RewardVersion(..), useFeaturedAppMarkers + ) import qualified Splice.AmuletConfig as Unit import Splice.ExternalPartyConfigState import Splice.Schedule @@ -91,6 +97,16 @@ data AmuletRules_MiningRound_ArchiveResult = AmuletRules_MiningRound_ArchiveResu data AmuletRules_ClaimExpiredRewardsResult = AmuletRules_ClaimExpiredRewardsResult with unclaimedRewardCid : Optional (ContractId UnclaimedReward) +-- The following results are intentionally empty to save on storage cost incurred in Scan. +-- These choices are driven by automation, which reads its results indirectly via the update to their backing stores. +data AmuletRules_ClaimExpiredRewardsV2Result = AmuletRules_ClaimExpiredRewardsV2Result {} + +data AmuletRules_StartProcessingRewardsV2Result = AmuletRules_StartProcessingRewardsV2Result {} + +data AmuletRules_UnhideRewardCouponsV2Result = AmuletRules_UnhideRewardCouponsV2Result {} + +data AmuletRules_ArchiveDryRunRewardAccountingV2Result = AmuletRules_ArchiveDryRunRewardAccountingV2Result {} + data AmuletRules_MergeUnclaimedRewardsResult = AmuletRules_MergeUnclaimedRewardsResult with unclaimedRewardCid : ContractId UnclaimedReward @@ -372,6 +388,11 @@ template AmuletRules now <- getTime let configUsd = getValueAsOf now configSchedule let tickDuration = configUsd.tickDuration + let rewardConfig = configUsd.rewardConfig + let trafficPrice = + if isSome rewardConfig + then Some configUsd.decentralizedSynchronizer.fees.extraTrafficPrice + else None -- Keep OpenMiningRound downgradable to prior versions for as long as possible let nr0 = fromOptional 0 initialRound let nr1 = nr0 + 1 let nr2 = nr1 + 1 @@ -401,10 +422,13 @@ template AmuletRules oldest <- create OpenMiningRound with dso; round = Round nr0; amuletPrice; opensAt = opensAt0; targetClosesAt = targetClosesAt0; issuingFor = issuingFor0; transferConfigUsd ; tickDuration ; issuanceConfig = issuanceConfig0 + trafficPrice; rewardConfig newestOpen <- create OpenMiningRound with dso; round = Round nr1; amuletPrice; opensAt = opensAt1; targetClosesAt = targetClosesAt1; issuingFor = issuingFor1; transferConfigUsd ; tickDuration ; issuanceConfig = issuanceConfig1 + trafficPrice; rewardConfig last <- create OpenMiningRound with dso; round = Round nr2; amuletPrice; opensAt = opensAt2; targetClosesAt = targetClosesAt2; issuingFor = issuingFor2; transferConfigUsd ; tickDuration ; issuanceConfig = issuanceConfig2 + trafficPrice; rewardConfig exercise self AmuletRules_BootstrapExternalPartyConfigState with openMiningRoundTriple = OpenMiningRoundTriple with @@ -446,19 +470,40 @@ template AmuletRules require "latestRound is open" (latestRound.opensAt <= now) require "middle round has been open for >= 1 tick" (addRelTime middleRound.opensAt middleRound.tickDuration <= now) - -- archive and create the rounds + -- create the right state to start summarizing the round newSummarizingRound <- create SummarizingMiningRound with dso round = roundToArchive.round amuletPrice = roundToArchive.amuletPrice issuanceConfig = roundToArchive.issuanceConfig tickDuration = roundToArchive.tickDuration + trafficPrice = roundToArchive.trafficPrice + rewardConfig = roundToArchive.rewardConfig + + -- trigger off-ledger reward calculation if needed + forA_ roundToArchive.rewardConfig $ \rewardConfig -> do + when (useTrafficBasedAppRewards (Some rewardConfig.mintingVersion)) $ do + void $ create CalculateRewardsV2 with + dso + round = roundToArchive.round + rewardCouponTimeToLive = rewardConfig.rewardCouponTimeToLive + dryRun = False + + when (useTrafficBasedAppRewards rewardConfig.dryRunVersion) $ do + void $ create CalculateRewardsV2 with + dso + round = roundToArchive.round + rewardCouponTimeToLive = rewardConfig.rewardCouponTimeToLive + dryRun = True + + -- create the new open round newOpenRound <- do let configUsd = getValueAsOf now configSchedule tickDuration = configUsd.tickDuration -- round is in pre-fetchable state for at least 1 tick and can only open with 1-tick difference between latestRound's opensAt opensAt = addRelTime (max now latestRound.opensAt) tickDuration newOpenRoundIssuingFor = latestRound.issuingFor + latestRound.tickDuration + rewardConfig = configUsd.rewardConfig create OpenMiningRound with dso round = Round (latestRound.round.number + 1) @@ -472,6 +517,11 @@ template AmuletRules transferConfigUsd = configUsd.transferConfig issuanceConfig = getValueAsOf newOpenRoundIssuingFor configUsd.issuanceCurve tickDuration + rewardConfig + trafficPrice = + if isSome rewardConfig + then Some configUsd.decentralizedSynchronizer.fees.extraTrafficPrice + else None -- Keep OpenMiningRound downgradable to prior versions for as long as possible return AmuletRules_AdvanceOpenMiningRoundsResult with summarizingRoundCid = newSummarizingRound openRoundCid = newOpenRound @@ -611,6 +661,84 @@ template AmuletRules else Some <$> create UnclaimedReward with dso; amount return AmuletRules_ClaimExpiredRewardsResult with unclaimedRewardCid + -- batch claim of expired rewards that use time-based expiry + nonconsuming choice AmuletRules_ClaimExpiredRewardsV2 : AmuletRules_ClaimExpiredRewardsV2Result + with + rewardCouponCids : [ContractId RewardCouponV2] + beneficiaries : [Party] + observer beneficiaries + controller dso + do + require "at least one coupon" (not (null rewardCouponCids)) + + -- archive coupons + coupons <- forA rewardCouponCids $ \cid -> do + coupon <- fetchAndArchive (ForDso with dso) cid + assertDeadlineExceeded "coupon.expiresAt" coupon.expiresAt + return coupon + let actualBeneficiaries = [ fromOptional coupon.provider coupon.beneficiary | coupon <- coupons ] + require "beneficiaries match coupons" (sort beneficiaries == dedupSort actualBeneficiaries) + + -- create unclaimed reward for the total + let amount = sum [ coupon.amount | coupon <- coupons ] + when (amount > 0.0) $ + void $ create UnclaimedReward with dso; amount + return AmuletRules_ClaimExpiredRewardsV2Result {} + + nonconsuming choice AmuletRules_StartProcessingRewardsV2 : AmuletRules_StartProcessingRewardsV2Result + with + calculateRewardsCid : ContractId CalculateRewardsV2 + batchHash : CryptoHash.Hash + controller dso + do + calculateRewards <- fetchAndArchive (ForDso with dso) calculateRewardsCid + create ProcessRewardsV2 with + dso + round = calculateRewards.round + dryRun = calculateRewards.dryRun + rewardCouponTimeToLive = calculateRewards.rewardCouponTimeToLive + batchHash + + return AmuletRules_StartProcessingRewardsV2Result {} + + -- batch conversion of coupons not yet observable by their beneficiaries + nonconsuming choice AmuletRules_UnhideRewardCouponsV2 : AmuletRules_UnhideRewardCouponsV2Result + with + rewardCouponCids : [ContractId RewardCouponV2] + beneficiaries : [Party] + observer beneficiaries + controller dso + do + require "at least one coupon" (not (null rewardCouponCids)) + + -- unhide coupons + actualBeneficiaries <- forA rewardCouponCids $ \cid -> do + coupon <- fetchAndArchive (ForDso with dso) cid + require "provider is no observer on the coupon" (not (coupon.providerIsObserver)) + assertWithinDeadline "coupon.expiresAt" coupon.expiresAt + create coupon with providerIsObserver = True + pure coupon.provider + + -- check specified beneficiaries match actual beneficiaries of the coupons + require "beneficiaries match coupons" (dedupSort beneficiaries == dedupSort actualBeneficiaries) + + return AmuletRules_UnhideRewardCouponsV2Result {} + + -- Choice to cleanup the state of stuck dry-runs in case getting them unstuck is too expensive. + nonconsuming choice AmuletRules_ArchiveDryRunRewardAccountingV2 : AmuletRules_ArchiveDryRunRewardAccountingV2Result + with + calculateRewardsCids : [ContractId CalculateRewardsV2] + processRewardsCids : [ContractId ProcessRewardsV2] + controller dso + do + forA_ calculateRewardsCids $ \calculateRewardsCid -> do + state <- fetchAndArchive (ForDso with dso) calculateRewardsCid + require "CalculateRewardsV2 is in dry-run state" (state.dryRun) + forA_ processRewardsCids $ \processRewardsCid -> do + state <- fetchAndArchive (ForDso with dso) processRewardsCid + require "ProcessRewardsV2 is in dry-run state" (state.dryRun) + return AmuletRules_ArchiveDryRunRewardAccountingV2Result {} + -- Batch merge of unclaimed rewards nonconsuming choice AmuletRules_MergeUnclaimedRewards : AmuletRules_MergeUnclaimedRewardsResult with @@ -749,27 +877,34 @@ template AmuletRules observers : Optional [Party] -- ^ A list of choice observers. This is expected to be set to the union of all providers and beneficiaries to ensure that this creates only one view. observer fromOptional [] observers controller dso - do now <- getTime - markers <- mapA (fetchAndArchive (ForDso dso)) markerCids - let groupedMarkers = Map.fromListWithR (+) (map (\m -> ((m.provider, m.beneficiary), m.weight)) markers) - round <- fetchReferenceData (ForDso dso) openMiningRoundCid - require ("mining round is open: " <> show round) (round.opensAt <= now) - let configUsd = getValueAsOf now configSchedule - -- If the amount is not set or is <= 0 we just archive the marker contracts. - markerCids <- forA (optionalToList configUsd.featuredAppActivityMarkerAmount) $ \amountUsd -> do - let amountAmulet = amountUsd / round.amuletPrice - if amountAmulet > 0.0 - then - forA (Map.toList groupedMarkers) $ \((provider, beneficiary), weight) -> - create AppRewardCoupon with - dso - provider - beneficiary = Some beneficiary - featured = True - round = round.round - amount = amountAmulet * weight - else pure [] - pure AmuletRules_ConvertFeaturedAppActivityMarkersResult with appRewardCouponCids = concat markerCids + do + now <- getTime + markers <- mapA (fetchAndArchive (ForDso dso)) markerCids + let groupedMarkers = Map.fromListWithR (+) (map (\m -> ((m.provider, m.beneficiary), m.weight)) markers) + round <- fetchReferenceData (ForDso dso) openMiningRoundCid + require ("mining round is open: " <> show round) (round.opensAt <= now) + let configUsd = getValueAsOf now configSchedule + if useTrafficBasedAppRewards ((.mintingVersion) <$> round.rewardConfig) + then + -- Markers may still be created for rounds that use traffic-based app rewards + -- ==> just archive the markers. + pure AmuletRules_ConvertFeaturedAppActivityMarkersResult with appRewardCouponCids = [] + else do + -- If the amount is not set or is <= 0 we just archive the marker contracts. + markerCids <- forA (optionalToList configUsd.featuredAppActivityMarkerAmount) $ \amountUsd -> do + let amountAmulet = amountUsd / round.amuletPrice + if amountAmulet > 0.0 + then + forA (Map.toList groupedMarkers) $ \((provider, beneficiary), weight) -> + create AppRewardCoupon with + dso + provider + beneficiary = Some beneficiary + featured = True + round = round.round + amount = amountAmulet * weight + else pure [] + pure AmuletRules_ConvertFeaturedAppActivityMarkersResult with appRewardCouponCids = concat markerCids nonconsuming choice AmuletRules_Amulet_ExpireTransferInstructions : AmuletRules_Amulet_ExpireTransferInstructionsResult with @@ -820,8 +955,10 @@ template AmuletRules holdingFeesOpenRoundNumber = validatedRounds.oldestRound.round amuletPrice = validatedRounds.latestUsableRound.amuletPrice transferConfig = transferConfigToTransferConfigV2 validatedRounds.latestUsableRound.transferConfigUsd + rewardCalculationVersion = (.mintingVersion) <$> validatedRounds.latestUsableRound.rewardConfig pure AmuletRules_UpdateExternalPartyConfigStatesResult with .. + data OpenMiningRoundTriple = OpenMiningRoundTriple with round0Cid : ContractId OpenMiningRound @@ -847,6 +984,9 @@ validateOpenMiningRoundTriple dso roundTriple = do oldestRound = round0 latestUsableRound = maximumOn (.round.number) usableRounds +miningRoundTripleCids : OpenMiningRoundTriple -> [ContractId OpenMiningRound] +miningRoundTripleCids OpenMiningRoundTriple {..} = [round0Cid, round1Cid, round2Cid] + -- Transfer logic -- ============== @@ -965,6 +1105,7 @@ data TransferContextSummaryV2 = TransferContextSummaryV2 with amuletPrice : Decimal issuingMiningRounds : Map Round IssuingMiningRound validatorRights : Map Party (ContractId ValidatorRight) + rewardCalculationVersion : Optional RewardVersion deriving (Eq, Show) data TransferInputsSummary = TransferInputsSummary with @@ -1024,6 +1165,7 @@ summarizeAndValidateContext context dso tf = do issuingMiningRounds = Map.fromList issuingMiningRounds validatorRights = Map.fromList validatorRights amuletPrice = openRound.amuletPrice + rewardCalculationVersion = (.mintingVersion) <$> openRound.rewardConfig summarizeAndValidateExternalPartyContext : ExternalPartyTransferContext -> Party -> Transfer -> Update TransferContextSummaryV2 summarizeAndValidateExternalPartyContext context dso tf = do @@ -1040,10 +1182,11 @@ summarizeAndValidateExternalPartyContext context dso tf = do config = scaleFees2 (1.0 / externalPartyConfigState.amuletPrice) $ externalPartyConfigState.transferConfig featuredAppProvider openRoundNumber = externalPartyConfigState.holdingFeesOpenRoundNumber - amuletPrice = externalPartyConfigState.amuletPrice -- no minting for long lived transfers issuingMiningRounds = Map.empty validatorRights = Map.empty + amuletPrice = externalPartyConfigState.amuletPrice + rewardCalculationVersion = externalPartyConfigState.rewardCalculationVersion getValidatorRight : TransferContextSummaryV2 -> Party -> Update (ContractId ValidatorRight) getValidatorRight csum user = @@ -1205,6 +1348,23 @@ summarizeAndConsumeInputs csum dso sender inps = do changeToHoldingFeesRate = s.changeToHoldingFeesRate totalDevelopmentFundAmount = (+ coupon.amount) <$> s.totalDevelopmentFundAmount + summarizeAndConsumeInput _round s (InputRewardCouponV2 couponCid) = do + coupon <- fetchAndArchive forOwner couponCid + assertWithinDeadline "RewardCouponV2.expiresAt" coupon.expiresAt + return TransferInputsSummary with + totalAmuletAmount = s.totalAmuletAmount + -- Note: in the current implementation RewardCouponV2 is only used for app rewards. + -- This may change in the future. As part of such a change we'll adjust the attribution done here. + totalAppRewardAmount = s.totalAppRewardAmount + coupon.amount + totalValidatorRewardAmount = s.totalValidatorRewardAmount + totalUnclaimedActivityRecordAmount = s.totalUnclaimedActivityRecordAmount + totalValidatorFaucetAmount = s.totalValidatorFaucetAmount + totalSvRewardAmount = s.totalSvRewardAmount + totalHoldingFees = s.totalHoldingFees + amountArchivedAsOfRoundZero = s.amountArchivedAsOfRoundZero + changeToHoldingFeesRate = s.changeToHoldingFeesRate + totalDevelopmentFundAmount = s.totalDevelopmentFundAmount + summarizeAndConsumeValidatorFaucetInput s couponCid = do coupon <- fetchAndArchive forOwner couponCid -- compute balance change @@ -1311,22 +1471,24 @@ summarizeTransfer sender openRoundNumber amuletPrice transferConfigAmulet inp pr amuletPrice issueRewards : RewardsIssuanceConfig -> TransferContextSummaryV2 -> Party -> Optional [AppRewardBeneficiary] -> Update () -issueRewards config csum provider beneficiaries = do - if config.issueAppRewards - then do - let beneficiaries' = fromOptional [AppRewardBeneficiary provider 1.0] beneficiaries - validateAppRewardBeneficiaries beneficiaries' - let groupedBeneficiaries = Map.fromListWithR (+) (map (\b -> (b.beneficiary, b.weight)) beneficiaries') - when featured $ - forA_ (Map.toList groupedBeneficiaries) $ \(beneficiary, weight) -> - void $ create FeaturedAppActivityMarker - with - dso = csum.dso - provider - beneficiary = beneficiary - weight = weight - else do - require "beneficiaries are unset if issueAppRewards is false" (optional True null beneficiaries) +issueRewards config csum provider beneficiaries + | useFeaturedAppMarkers csum.rewardCalculationVersion = do + if config.issueAppRewards + then do + let beneficiaries' = fromOptional [AppRewardBeneficiary provider 1.0] beneficiaries + validateAppRewardBeneficiaries beneficiaries' + let groupedBeneficiaries = Map.fromListWithR (+) (map (\b -> (b.beneficiary, b.weight)) beneficiaries') + when featured $ + forA_ (Map.toList groupedBeneficiaries) $ \(beneficiary, weight) -> + void $ create FeaturedAppActivityMarker + with + dso = csum.dso + provider + beneficiary = beneficiary + weight = weight + else do + require "beneficiaries are unset if issueAppRewards is false" (optional True null beneficiaries) + | otherwise = pure () where featured = Some provider == csum.featuredAppProvider @@ -1389,12 +1551,13 @@ validateBuyMemberTrafficInputs configUsd synchronizerId trafficAmount -- | Computing synchronizer fees computeSynchronizerFees : Party -> Party -> Int -> AmuletRules -> TransferContext -> Update (Decimal, Decimal) computeSynchronizerFees dso validator trafficAmount amuletRules context = do + contextMiningRound <- fetchPublicReferenceData (ForDso dso) context.openMiningRound (OpenMiningRound_Fetch validator) -- compute traffic cost in USD configUsd <- getValueAsOfLedgerTime amuletRules.configSchedule - let extraTrafficPrice = configUsd.decentralizedSynchronizer.fees.extraTrafficPrice + let extraTrafficPrice = + fromOptional (configUsd.decentralizedSynchronizer.fees.extraTrafficPrice) contextMiningRound.trafficPrice let trafficCostUsd = intToDecimal trafficAmount / 1e6 * extraTrafficPrice -- compute traffic cost in Amulet - contextMiningRound <- fetchPublicReferenceData (ForDso dso) context.openMiningRound (OpenMiningRound_Fetch validator) let trafficCostAmulet = trafficCostUsd / contextMiningRound.amuletPrice pure (trafficCostAmulet, trafficCostUsd) @@ -1500,6 +1663,7 @@ data TransferInput | InputValidatorLivenessActivityRecord (ContractId ValidatorLivenessActivityRecord) | InputUnclaimedActivityRecord (ContractId UnclaimedActivityRecord) | InputDevelopmentFundCoupon (ContractId DevelopmentFundCoupon) + | InputRewardCouponV2 (ContractId RewardCouponV2) deriving (Eq, Ord, Show) -- | Smart constructor for inputing validator faucet coupons into a transfer. @@ -1941,6 +2105,7 @@ bootstrapExternalPartyConfigState AmuletRules{..} openMiningRoundTriple = do holdingFeesOpenRoundNumber = validatedRounds.oldestRound.round amuletPrice = validatedRounds.latestUsableRound.amuletPrice transferConfig = transferConfig + rewardCalculationVersion = (.mintingVersion) <$> validatedRounds.latestUsableRound.rewardConfig configState1 = configState0 with targetArchiveAfter = configState0.targetArchiveAfter `addRelTime` getExternalPartyConfigStateTickDuration config create configState0 diff --git a/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml b/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml index 385003f39d..b35a18056c 100644 --- a/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml @@ -21,8 +21,6 @@ import Splice.AmuletRules import Splice.Types import Splice.Util - - import DA.Exception diff --git a/daml/splice-amulet/daml/Splice/ExternalPartyConfigState.daml b/daml/splice-amulet/daml/Splice/ExternalPartyConfigState.daml index 784c4cc213..02835994a3 100644 --- a/daml/splice-amulet/daml/Splice/ExternalPartyConfigState.daml +++ b/daml/splice-amulet/daml/Splice/ExternalPartyConfigState.daml @@ -21,6 +21,7 @@ template ExternalPartyConfigState amuletPrice : Decimal -- ^ Amulet price at the time the config state was created. transferConfig : TransferConfigV2 Unit.USD -- ^ Transfer config at the time the config state was created. targetArchiveAfter : Time -- ^ Lower bound on the time the contract gets archived, not enforced as a strict upper bound. + rewardCalculationVersion : Optional RewardVersion -- ^ The reward calculation version to use for transactions relying on this config state. where signatory dso diff --git a/daml/splice-amulet/daml/Splice/Round.daml b/daml/splice-amulet/daml/Splice/Round.daml index 20f985f54f..122cc27ff3 100644 --- a/daml/splice-amulet/daml/Splice/Round.daml +++ b/daml/splice-amulet/daml/Splice/Round.daml @@ -28,6 +28,12 @@ template OpenMiningRound transferConfigUsd : TransferConfig USD -- ^ Configuration determining the fees and limits in USD for Amulet transfers issuanceConfig : IssuanceConfig -- ^ Configuration for issuance of this round. tickDuration : RelTime -- ^ Duration of a tick, which is the duration of half a round. + trafficPrice : Optional Decimal + -- ^ Traffic price in $/MB at round start time. Used by the reward + -- calculation to translate traffic burn back to amulet. + rewardConfig : Optional RewardConfig + -- ^ Configuration for off-ledger reward calculation for this round. + -- If None, rewards are computed on-ledger using the pre-CIP-104 mechanism. where signatory dso ensure isDefinedRound round @@ -47,6 +53,8 @@ template SummarizingMiningRound amuletPrice : Decimal issuanceConfig : IssuanceConfig tickDuration : RelTime + trafficPrice : Optional Decimal + rewardConfig : Optional RewardConfig where signatory dso ensure isDefinedRound round diff --git a/daml/splice-api-reward-assignment-v1/daml.yaml b/daml/splice-api-reward-assignment-v1/daml.yaml new file mode 100644 index 0000000000..0e3376a658 --- /dev/null +++ b/daml/splice-api-reward-assignment-v1/daml.yaml @@ -0,0 +1,18 @@ +sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 +name: splice-api-reward-assignment-v1 +source: daml +version: 1.0.0 +dependencies: + - daml-prim + - daml-stdlib +data-dependencies: + - ../../token-standard/splice-api-token-metadata-v1/.daml/dist/splice-api-token-metadata-v1-current.dar +build-options: + - --ghc-option=-Wunused-binds + - --ghc-option=-Wunused-matches + - --target=2.1 +codegen: + java: + package-prefix: org.lfdecentralizedtrust.splice.codegen.java + decoderClass: org.lfdecentralizedtrust.splice.codegen.java.DecoderSpliceRewardAssignmentV1Interfaces + output-directory: target/daml-codegen-java diff --git a/daml/splice-api-reward-assignment-v1/daml/Splice/Api/RewardAssignmentV1.daml b/daml/splice-api-reward-assignment-v1/daml/Splice/Api/RewardAssignmentV1.daml new file mode 100644 index 0000000000..827e2526a5 --- /dev/null +++ b/daml/splice-api-reward-assignment-v1/daml/Splice/Api/RewardAssignmentV1.daml @@ -0,0 +1,71 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +-- | An API for app providers and other service providers to assign reward +-- coupons for their activity to their beneficiaries. +module Splice.Api.RewardAssignmentV1 where + +import Splice.Api.Token.MetadataV1 + +-- | View on a coupon representing the right to mint a certain amount of rewards. +data RewardCouponView = RewardCouponView + with + dso : Party + -- ^ The DSO party. + provider : Party + -- ^ The party that provided the service for whose activity the minting right was granted. + beneficiary : Party + -- ^ The beneficiary that can mint the amount specified in the coupon. + amount : Decimal + -- ^ Amulet amount that can be minted with this coupon. + expiresAt : Time + -- ^ Expiration time of the coupon. The minting right granted by the coupon can only be exercised before this time. + maxNumNewBeneficiaries : Int + -- ^ The maximum number of new beneficiaries that can be assigned to the coupon in a single assignment. + meta : Metadata + -- ^ Metadata associated with this coupon. Provided for extensibility. + deriving (Show, Eq) + +-- | Specification of a beneficiary of rewards. +data RewardBeneficiary = RewardBeneficiary + with + beneficiary : Party + -- ^ The party that is granted the right to mint amulet for this activity. + weight : Decimal + -- ^ A weight between 0.0 and 1.0 that defines how much of the reward this beneficiary can mint. + deriving (Show, Eq, Ord) + +-- | A coupon representing the right to mint a certain amount of rewards. +interface RewardCoupon where + viewtype RewardCouponView + + rewardCoupon_assignBeneficiariesImpl + : ContractId RewardCoupon -> RewardCoupon_AssignBeneficiaries -> Update RewardCoupon_AssignBeneficiariesResult + + choice RewardCoupon_AssignBeneficiaries : RewardCoupon_AssignBeneficiariesResult + -- ^ Assign (ultimate) beneficiaries to the coupon. Useful for apps + -- where the party that earns the minting right (the provider) is just an + -- operational party and the actual beneficiaries are different parties. + with + additionalCoupons : [ContractId RewardCoupon] + -- ^ Additional coupons of the provider to share in the same transaction. + newBeneficiaries : [RewardBeneficiary] + -- ^ The new beneficiaries to whom to assign part of the minting amount of the + -- coupon. The weights MUST be between 0.0 and 1.0, add up to 1.0, + -- and there MUST NOT be duplicate beneficiaries. + -- + -- There MUST be at most `(view this).maxNumNewBeneficiaries` new beneficiaries in the list. + -- The purpose of this limit is to ensure that traffic cost of + -- creating coupons guards the overhead of tracking the created coupons + -- for the DSO party. + extraArgs : ExtraArgs + -- ^ Extra arguments for extensibility. Set to empty, unless needed for specific implementations. + observer map (.beneficiary) newBeneficiaries + controller (view this).provider + do rewardCoupon_assignBeneficiariesImpl this self arg + + +data RewardCoupon_AssignBeneficiariesResult = RewardCoupon_AssignBeneficiariesResult with + newBeneficiariesCouponCids : [(Party, [ContractId RewardCoupon])] + -- ^ The coupons created for the newly assigned beneficiaries. + deriving (Eq, Show) diff --git a/daml/splice-dso-governance-test/daml.yaml b/daml/splice-dso-governance-test/daml.yaml index 0e0acfcb9f..e020d645ad 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.28 +version: 0.1.29 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestRewardAccountingV2.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestRewardAccountingV2.daml new file mode 100644 index 0000000000..837af8463d --- /dev/null +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestRewardAccountingV2.daml @@ -0,0 +1,215 @@ +-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +module Splice.Scripts.DsoTestRewardAccountingV2 where + + +import DA.Action (void) +import DA.Assert +import DA.Foldable (forA_) +import DA.List +import DA.Set as Set +import DA.Time + +import Daml.Script + +import Splice.Amulet +import Splice.Amulet.RewardAccountingV2 +import Splice.Amulet.CryptoHash qualified as CryptoHash +import Splice.AmuletConfig +import Splice.AmuletRules +import Splice.DsoRules +import Splice.Types + +import Splice.Scripts.Util +import Splice.Scripts.DsoTestUtils + +import Splice.Testing.Registries.AmuletRegistry.Parameters (defaultAmuletConfig) + + +-- | Shared setup for reward accounting tests, which initiates the reward processing workflow and sets up demo data. +initiate_reward_processing : Bool -> Script (Script (), AmuletApp, (Party, Party, Party, Party, Party)) +initiate_reward_processing dryRun = do + -- enable traffic based app rewards, which are the first use-case for reward accounting v2 + let amuletConfig = defaultAmuletConfig with + rewardConfig = Some $ RewardConfig with + mintingVersion = if dryRun then RewardVersion_FeaturedAppMarkers else RewardVersion_TrafficBasedAppRewards + dryRunVersion = if dryRun then Some RewardVersion_TrafficBasedAppRewards else None + batchSize = 100 + rewardCouponTimeToLive = hours 36 + appRewardCouponThreshold = 0.5 + + (app, _, (sv1, _, _, _)) <- initDevNetWithAmuletConfig amuletConfig + + alice <- allocateParty "Alice" + bob <- allocateParty "Bob" + charlie <- allocateParty "Charlie" + dora <- allocateParty "Dora" + + setTime demoTime + + -- move the first round through issuance, which will also trigger the reward calculation for this round + if dryRun then runNextIssuanceD app 1.0 None + else runNextIssuanceD app 1.0 (Some 1.2) + + -- setup demo data + let mintingAllowances1 = sortOn (.provider) + [ MintingAllowance alice 1000.0 + , MintingAllowance bob 2000.0 + ] + let mintingAllowances2 = sortOn (.provider) + [ MintingAllowance charlie 30.0 + , MintingAllowance dora 5.1 + ] + let b1 = BatchOfMintingAllowances mintingAllowances1 + let b2 = BatchOfMintingAllowances mintingAllowances2 + let rootBatch = BatchOfBatches [CryptoHash.hash b1, CryptoHash.hash b2] + let rootBatchHash = CryptoHash.hash rootBatch + let batchesWithHiding = [(b1, [bob]), (b2, [dora]), (rootBatch, [])] + + -- get the contract representing the pending calculation and confirmation of rewards for round 0 + [(calculateRewardsCid, _)] <- query @CalculateRewardsV2 app.dso + + -- setup reward coupon creation workflow state + confirmAndExecutionAction app ARC_AmuletRules with + amuletRulesAction = CRARC_StartProcessingRewardsV2 + AmuletRules_StartProcessingRewardsV2 with + calculateRewardsCid + batchHash = rootBatchHash + + [(dsoRulesCid, _)] <- query @DsoRules app.dso + + let processBatches = do + states <- query @ProcessRewardsV2 app.dso + forA_ states $ \(processRewardsCid, processRewards) -> do + let Some (b, badVettingState) = find (\(b, _) -> CryptoHash.hash b == processRewards.batchHash) batchesWithHiding + void $ submitMulti [sv1] [app.dso] $ exerciseCmd dsoRulesCid DsoRules_ProcessRewardsV2_ProcessBatch with + sv = sv1 + processRewardsCid + choiceArg = ProcessRewardsV2_ProcessBatch with + batch = b + providersWithWrongVettingState = Set.fromList badVettingState + + pure (processBatches, app, (sv1, alice, bob, charlie, dora)) + + +-- | Test that reward accounting can be driven via DsoRules. +-- +-- We focus on the core logic only, as the extensive functional testing is done +-- directly on `AmuletRules` in `TestRewardAccountingV2`, and the choices in DsoRules +-- are pass-through choices to `AmuletRules` that don't have any additional logic. +test_reward_accounting_v2 : Script () +test_reward_accounting_v2 = do + (processBatches, app, (sv1, alice, bob, charlie, dora)) <- initiate_reward_processing False + + processBatches -- expand root hash into batch hashes + processBatches -- expand follow-up batches into coupons + + let couponExpiryTime = demoTime `addRelTime` hours 36 + let expectedAmounts = [(alice, 1000.0), (bob, 2000.0), (charlie, 30.0), (dora, 5.1)] + let expectedCoupons = do + (provider, amount) <- expectedAmounts + pure RewardCouponV2 with + dso = app.dso + provider + amount + round = Round 0 + expiresAt = couponExpiryTime + providerIsObserver = provider `notElem` [bob, dora] + beneficiary = None + + actualCoupons0 <- query @RewardCouponV2 app.dso + let actualCoupons = sortOn (.provider) $ fmap snd actualCoupons0 + actualCoupons === expectedCoupons + + -- make Bob and Dora observers of their coupons (simulates them changing their vetting state) + [(dsoRulesCid, _)] <- query @DsoRules app.dso + [(amuletRulesCid, _)] <- query @AmuletRules app.dso + unobservableCoupons <- queryFilter @RewardCouponV2 app.dso (\c -> not c.providerIsObserver) + void $ submitMulti [sv1] [app.dso] $ exerciseCmd dsoRulesCid DsoRules_UnhideRewardCouponsV2 with + sv = sv1 + amuletRulesCid + choiceArg = AmuletRules_UnhideRewardCouponsV2 with + rewardCouponCids = map fst unobservableCoupons + beneficiaries = map (._2.provider) unobservableCoupons + + couponsAfterUnhiding <- query @RewardCouponV2 app.dso + sortOn (.provider) (map snd couponsAfterUnhiding) === + map (\c -> c with providerIsObserver = True) expectedCoupons + + pure () + + +-- Test that the choice forwarding to AmuletRules works correctly +----------------------------------------------------------------- + +test_ClaimExpiredRewardsV2 : Script () +test_ClaimExpiredRewardsV2 = do + (app, _, (sv1, _, _, _)) <- initDevNet + + alice <- allocateParty "Alice" + bob <- allocateParty "Bob" + + + -- create coupons + forA_ [ (alice, Some alice, 100.0), (alice, Some bob, 500.0) ] $ \(provider, beneficiary, amount) -> do + submit app.dso $ createCmd RewardCouponV2 with + dso = app.dso + provider + amount + round = Round 0 + expiresAt = demoTime `addRelTime` hours 48 + providerIsObserver = False + beneficiary + + -- expiry works + setTime $ demoTime `addRelTime` hours 48 + [(amuletRulesCid, _)] <- query @AmuletRules app.dso + [(dsoRulesCid, _)] <- query @DsoRules app.dso + rewardCouponCids <- query @RewardCouponV2 app.dso + + submitMulti [sv1] [app.dso] $ exerciseCmd dsoRulesCid DsoRules_ClaimExpiredRewardsV2 with + amuletRulesCid + choiceArg = AmuletRules_ClaimExpiredRewardsV2 with + rewardCouponCids = map fst rewardCouponCids + beneficiaries = [bob, alice] + sv = sv1 + + -- no coupons left + [] <- query @RewardCouponV2 app.dso + + pure () + + +test_ArchiveDryRunRewardAccountingV2 : Script () +test_ArchiveDryRunRewardAccountingV2 = do + (processBatches, app, (sv1, _, _, _, _)) <- initiate_reward_processing True + + processBatches -- expand root hash into batch hashes + -- pretend that follow up batches fail to process due to hash mismatches + + -- move to next round, so there's also a calculate rewards contract + runNextIssuance app + + -- archive the stuck state + processRewards <- query @ProcessRewardsV2 app.dso + calculateRewards <- query @CalculateRewardsV2 app.dso + + [(amuletRulesCid, _)] <- query @AmuletRules app.dso + [(dsoRulesCid, _)] <- query @DsoRules app.dso + + void $ submitMulti [sv1] [app.dso] $ exerciseCmd dsoRulesCid DsoRules_ArchiveDryRunRewardAccountingV2 with + sv = sv1 + amuletRulesCid + choiceArg = AmuletRules_ArchiveDryRunRewardAccountingV2 with + processRewardsCids = map fst processRewards + calculateRewardsCids = map fst calculateRewards + + -- no left-over processing contracts + [] <- query @CalculateRewardsV2 app.dso + [] <- query @ProcessRewardsV2 app.dso + + -- check that no coupons were created + [] <- query @RewardCouponV2 app.dso + pure () + 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 16eab814c7..eab05aee69 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml @@ -16,7 +16,7 @@ import Daml.Script import DA.Time import Splice.Amulet -import Splice.AmuletConfig (AmuletConfig(..), USD) +import Splice.AmuletConfig (AmuletConfig(..), USD, useTrafficBasedAppRewards) import Splice.AmuletRules import Splice.Issuance import Splice.Round @@ -132,11 +132,7 @@ initDecentralizedSynchronizerWithAmuletPrice isDevNet initialRound amuletPrice o DsoBootstrap_Bootstrap dsoUserId <- validateUserId "dso-user" - let app = AmuletApp with - dso - dsoUser = AmuletUser with - userId = dsoUserId - primaryParty = dso + let app = mkAmuletApp dso (AmuletUser dsoUserId dso) -- add more sv nodes forA_ (zip [sv2, sv3, sv4] ["sv2", "sv3", "sv4"]) $ \(svParty, svName) -> do @@ -177,7 +173,7 @@ generateUnclaimedReward app provider1 = do round = Round 0 featured = True - runNextIssuanceD app 1.0 + runNextIssuanceD app 1.0 None pure () @@ -207,8 +203,8 @@ onboardValidator app sponsor validatorName validator = do -- | Run the next issuance. -runNextIssuanceD : AmuletApp -> Decimal -> Script () -runNextIssuanceD app amuletPrice = do +runNextIssuanceD : AmuletApp -> Decimal -> Optional Decimal -> Script () +runNextIssuanceD app amuletPrice appActivityRoundTotal = do advanceToNextRoundChange app.dso -- expire rewards for closed rounds closedRounds <- query @ClosedMiningRound app.dso @@ -262,6 +258,8 @@ runNextIssuanceD app amuletPrice = do sv = Some (head (Map.keys dsoRules.svs)) let summarizingRoundCid = advanceResult.summarizingRound + Some summarizingRound <- queryContractId app.dso summarizingRoundCid + -- compute total burn appRewardCoupons <- queryFilter @AppRewardCoupon app.dso (\bc -> bc.round == roundToArchive.round) validatorRewardCoupons <- queryFilter @ValidatorRewardCoupon app.dso (\bc -> bc.round == roundToArchive.round) @@ -269,10 +267,27 @@ runNextIssuanceD app amuletPrice = do validatorLivenessActivityRecords <- queryFilter @ValidatorLivenessActivityRecord app.dso (\bc -> bc.round == roundToArchive.round) svRewardCoupons <- queryFilter @SvRewardCoupon app.dso (\bc -> bc.round == roundToArchive.round) + let trafficBasedRewards = useTrafficBasedAppRewards ((.mintingVersion) <$> summarizingRound.rewardConfig) + + -- compute app reward totals: for traffic-based rewards, convert activity (MB) to Amulet + -- via trafficPrice ($/MB) and amuletPrice ($/Amulet); otherwise use on-ledger coupons + (totalFeaturedCoupons, totalUnfeaturedCoupons) <- + if trafficBasedRewards then + case (appActivityRoundTotal, summarizingRound.trafficPrice) of + (Some activity, Some trafficPrice) -> + pure (activity * trafficPrice / summarizingRound.amuletPrice, 0.0) + (None, _) -> + fail "appActivityRoundTotal must be specified when using traffic-based app rewards" + (_, None) -> + fail "trafficPrice must be specified when using traffic-based app rewards" + else + pure (sum [ c.amount | (_, c) <- appRewardCoupons, c.featured] + , sum [ c.amount | (_, c) <- appRewardCoupons, not (c.featured)]) + let summary = OpenMiningRoundSummary with totalValidatorRewardCoupons = sum [ c.amount | (_, c) <- validatorRewardCoupons] - totalFeaturedAppRewardCoupons = sum [ c.amount | (_, c) <- appRewardCoupons, c.featured] - totalUnfeaturedAppRewardCoupons = sum [ c.amount | (_, c) <- appRewardCoupons, not (c.featured)] + totalFeaturedAppRewardCoupons = totalFeaturedCoupons + totalUnfeaturedAppRewardCoupons = totalUnfeaturedCoupons totalSvRewardWeight = sum [ c.weight | (_, c) <- svRewardCoupons] optTotalValidatorFaucetCoupons = Some (length validatorFaucetCoupons + length validatorLivenessActivityRecords) @@ -303,6 +318,19 @@ confirmAWC_MiningRound_Archive app = do confirmer = sv pure () +-- | Convenience function to confirm and execute an action requiring confirmation. +confirmAndExecutionAction : AmuletApp -> ActionRequiringConfirmation -> Script () +confirmAndExecutionAction app action = do + [(dsoRulesCid, rules)] <- query @DsoRules app.dso + forA_ (Map.keys rules.svs) $ \sv -> do + -- mallory does not act + unless ("mallory" `T.isPrefixOf` partyToText sv) $ do + submitMulti [sv] [app.dso] $ exerciseCmd dsoRulesCid DsoRules_ConfirmAction with + action + confirmer = sv + pure () + executeAllConfirmedActions app + executeAllConfirmedActions : AmuletApp -> Script () executeAllConfirmedActions app = do [(amuletRulesCid, _)] <- query @AmuletRules app.dso diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestDecentralizedAutomation.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestDecentralizedAutomation.daml index 45acadbc3e..d1390b87c6 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestDecentralizedAutomation.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestDecentralizedAutomation.daml @@ -85,7 +85,7 @@ testUnclaimedDevelopmentFundCouponsMerging = do (app, _, (sv1, _, _, _)) <- initDevNetWithAmuletConfig amuletConfig -- Mint 5 unclaimed development fund coupons - replicateA_ 5 $ runNextIssuanceD app 1.0 + replicateA_ 5 $ runNextIssuanceD app 1.0 None [(amuletRulesCid, _)] <- query @AmuletRules app.dso unclaimedDevelopmentFundCouponCids@(cid1 :: _) <- fmap fst <$> query @UnclaimedDevelopmentFundCoupon app.dso @@ -127,7 +127,7 @@ testDevelopmentFundCouponExpiry = do [(dsoRulesCid, _)] <- query @DsoRules app.dso -- Mint 1 unclaimed development fund coupon - runNextIssuanceD app 1.0 + runNextIssuanceD app 1.0 None [(unclaimedDevelopmentFundCouponCid, unclaimedDevelopmentFundCoupon)] <- query @UnclaimedDevelopmentFundCoupon app.dso -- Allocate a development fund coupon diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestFeaturedAppActivityMarkers.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestFeaturedAppActivityMarkers.daml index 1e6cd98ab2..d8336d1c59 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestFeaturedAppActivityMarkers.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestFeaturedAppActivityMarkers.daml @@ -134,8 +134,8 @@ testFeaturedAppActivityMarkers = do amount = 50.0 * defaultFeaturedAppActivityMarkerAmount / amuletPrice -- ensure that the round is in issuing phase - runNextIssuanceD app amuletPrice - runNextIssuanceD app amuletPrice + runNextIssuanceD app amuletPrice None + runNextIssuanceD app amuletPrice None -- advance until the opensAt advanceToNextRoundChange app.dso -- Check that coupons are archived, we don't check the detailed minting computations diff --git a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml index df550998c1..62744ad147 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/TestGovernance.daml @@ -718,7 +718,7 @@ testAmuletRulesTickDurationChange = do round2.tickDuration === initialConfig.tickDuration round3.tickDuration === initialConfig.tickDuration - runNextIssuanceD app 1.2 + runNextIssuanceD app 1.2 None -- check that tickDuraction has changed [(_, round1), (_, round2), (_, round3)] <- sortOn (._2.round) <$> query @OpenMiningRound dso @@ -1005,7 +1005,7 @@ testAmuletPriceVoting = do (app, dso, (sv1, sv2, sv3, sv4)) <- initMainNet -- run a normal issuance so time advances and the amulet price is set to 1.2 for everybody - runNextIssuanceD app 1.2 + runNextIssuanceD app 1.2 None -- have all SV's adjust their prices [(dsoRulesCid, _)] <- query @DsoRules dso diff --git a/daml/splice-dso-governance/daml.yaml b/daml/splice-dso-governance/daml.yaml index 68bfdc70e3..62d92e1092 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.24 +version: 0.1.25 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 299dac9c96..77b9f91c7d 100644 --- a/daml/splice-dso-governance/daml/Splice/DsoRules.daml +++ b/daml/splice-dso-governance/daml/Splice/DsoRules.daml @@ -18,6 +18,7 @@ import qualified DA.Text as T import DA.Time import Splice.Amulet +import Splice.Amulet.RewardAccountingV2 qualified as RewardAccountingV2 import Splice.AmuletRules import Splice.ExternalPartyAmuletRules @@ -91,6 +92,8 @@ data AmuletRules_ActionRequiringConfirmation -- ^ **Deprecated, use CRARC_SetConfig instead**: Voted action to update a config schedule in the `AmuletRules`. | CRARC_SetConfig AmuletRules_SetConfig -- ^ Voted action to change the `AmuletConfig`. Not idempotent. + | CRARC_StartProcessingRewardsV2 AmuletRules_StartProcessingRewardsV2 + -- ^ Automated action to start the processing of rewards where the minting allowances were computed off-ledger. deriving (Eq, Show) data DsoRules_ActionRequiringConfirmation @@ -268,6 +271,21 @@ data DsoRules_ReceiveSvRewardCouponResult = DsoRules_ReceiveSvRewardCouponResult data DsoRules_ClaimExpiredRewardsResult = DsoRules_ClaimExpiredRewardsResult with unclaimedReward: Optional (ContractId UnclaimedReward) +data DsoRules_ClaimExpiredRewardsV2Result = DsoRules_ClaimExpiredRewardsV2Result with + result : AmuletRules_ClaimExpiredRewardsV2Result + +data DsoRules_StartProcessingRewardsV2Result = DsoRules_StartProcessingRewardsV2Result with + result : AmuletRules_StartProcessingRewardsV2Result + +data DsoRules_ProcessRewardsV2_ProcessBatchResult = DsoRules_ProcessRewardsV2_ProcessBatchResult with + result : RewardAccountingV2.ProcessRewardsV2_ProcessBatchResult + +data DsoRules_UnhideRewardCouponsV2Result = DsoRules_UnhideRewardCouponsV2Result with + result : AmuletRules_UnhideRewardCouponsV2Result + +data DsoRules_ArchiveDryRunRewardAccountingV2Result = DsoRules_ArchiveDryRunRewardAccountingV2Result with + result : AmuletRules_ArchiveDryRunRewardAccountingV2Result + data DsoRules_MergeUnclaimedRewardsResult = DsoRules_MergeUnclaimedRewardsResult with unclaimedReward: ContractId UnclaimedReward @@ -1419,6 +1437,56 @@ template DsoRules with return DsoRules_ClaimExpiredRewardsResult with unclaimedReward = unclaimedRewardCid + nonconsuming choice DsoRules_ClaimExpiredRewardsV2 : DsoRules_ClaimExpiredRewardsV2Result + with + amuletRulesCid : ContractId AmuletRules + choiceArg : AmuletRules_ClaimExpiredRewardsV2 + sv : Party + controller sv + do + _ <- getAndValidateSvParty this (Some sv) + result <- exercise amuletRulesCid choiceArg + return DsoRules_ClaimExpiredRewardsV2Result with + result + + nonconsuming choice DsoRules_ProcessRewardsV2_ProcessBatch : DsoRules_ProcessRewardsV2_ProcessBatchResult + with + processRewardsCid : ContractId RewardAccountingV2.ProcessRewardsV2 + choiceArg : RewardAccountingV2.ProcessRewardsV2_ProcessBatch + sv : Party + controller sv + do + _ <- getAndValidateSvParty this (Some sv) + result <- exercise processRewardsCid choiceArg + return DsoRules_ProcessRewardsV2_ProcessBatchResult with + result + + nonconsuming choice DsoRules_UnhideRewardCouponsV2 : DsoRules_UnhideRewardCouponsV2Result + with + amuletRulesCid : ContractId AmuletRules + choiceArg : AmuletRules_UnhideRewardCouponsV2 + sv : Party + controller sv + do + _ <- getAndValidateSvParty this (Some sv) + result <- exercise amuletRulesCid choiceArg + return DsoRules_UnhideRewardCouponsV2Result with + result + + -- Choice to cleanup the state of stuck dry-runs in case getting them unstuck is too expensive. + nonconsuming choice DsoRules_ArchiveDryRunRewardAccountingV2 : DsoRules_ArchiveDryRunRewardAccountingV2Result + with + amuletRulesCid : ContractId AmuletRules + choiceArg : AmuletRules_ArchiveDryRunRewardAccountingV2 + sv : Party + controller sv + do + _ <- getAndValidateSvParty this (Some sv) + result <- exercise amuletRulesCid choiceArg + return DsoRules_ArchiveDryRunRewardAccountingV2Result with + result + + -- Batch merge of unclaimed rewards nonconsuming choice DsoRules_MergeUnclaimedRewards : DsoRules_MergeUnclaimedRewardsResult with @@ -1707,6 +1775,7 @@ executeActionRequiringConfirmation dso dsoRulesCid amuletRulesCid act = case act CRARC_AddFutureAmuletConfigSchedule choiceArg -> void $ exercise amuletRulesCid choiceArg CRARC_RemoveFutureAmuletConfigSchedule choiceArg -> void $ exercise amuletRulesCid choiceArg CRARC_UpdateFutureAmuletConfigSchedule choiceArg -> void $ exercise amuletRulesCid choiceArg + CRARC_StartProcessingRewardsV2 choiceArg -> void $ exercise amuletRulesCid choiceArg ARC_DsoRules with .. -> do void $ fetchChecked (ForDso with dso) dsoRulesCid case dsoAction of @@ -1774,6 +1843,7 @@ actionRequiringConfirmationEffectiveAt action = CRARC_UpdateFutureAmuletConfigSchedule choiceArg -> Some choiceArg.scheduleItem._1 CRARC_MiningRound_Archive _ -> None CRARC_MiningRound_StartIssuing _ -> None + CRARC_StartProcessingRewardsV2 _ -> None ARC_DsoRules with .. -> None ARC_AnsEntryContext with .. -> None ExtActionRequiringConformation _dummyUnitField -> diff --git a/daml/splice-util-batched-markers-test/daml.yaml b/daml/splice-util-batched-markers-test/daml.yaml index 876ffc1aa5..8232aa9fe5 100644 --- a/daml/splice-util-batched-markers-test/daml.yaml +++ b/daml/splice-util-batched-markers-test/daml.yaml @@ -1,7 +1,7 @@ sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 name: splice-util-batched-markers-test source: daml -version: 1.0.3 +version: 1.0.4 dependencies: - daml-prim - daml-script diff --git a/daml/splice-util-featured-app-proxies-test/daml.yaml b/daml/splice-util-featured-app-proxies-test/daml.yaml index 4fca4b09a1..c834854d8e 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.9 +version: 1.0.10 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 28d24ec46a..0d6d97dae5 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.4 +version: 1.0.5 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-wallet-payments/daml.yaml b/daml/splice-wallet-payments/daml.yaml index 586a456892..e5fc5f503f 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.17 +version: 0.1.18 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splice-wallet-test/daml.yaml b/daml/splice-wallet-test/daml.yaml index 3ea53ca28d..6a90a501af 100644 --- a/daml/splice-wallet-test/daml.yaml +++ b/daml/splice-wallet-test/daml.yaml @@ -1,12 +1,13 @@ sdk-version: 3.3.0-snapshot.20250502.13767.0.v2fc6c7e2 name: splice-wallet-test source: daml -version: 0.1.21 +version: 0.1.22 dependencies: - daml-prim - daml-stdlib - daml-script data-dependencies: +- ../splice-api-reward-assignment-v1/.daml/dist/splice-api-reward-assignment-v1-current.dar - ../splice-amulet/.daml/dist/splice-amulet-current.dar - ../splice-amulet-test/.daml/dist/splice-amulet-test-current.dar - ../splice-util/.daml/dist/splice-util-current.dar diff --git a/daml/splice-wallet-test/daml/Splice/Scripts/Wallet/TestMintingDelegation.daml b/daml/splice-wallet-test/daml/Splice/Scripts/Wallet/TestMintingDelegation.daml index 1e739ea8ab..82c90aceee 100644 --- a/daml/splice-wallet-test/daml/Splice/Scripts/Wallet/TestMintingDelegation.daml +++ b/daml/splice-wallet-test/daml/Splice/Scripts/Wallet/TestMintingDelegation.daml @@ -7,9 +7,12 @@ import DA.Time import Daml.Script import qualified DA.Map as Map +import DA.List (sort) import DA.List.Total (head) import Splice.Amulet import Splice.AmuletRules +import Splice.Api.RewardAssignmentV1 qualified as Api.RewardAssignmentV1 +import Splice.Testing.Utils (emptyExtraArgs) import Splice.Round import Splice.Util import Splice.Wallet.MintingDelegation @@ -83,6 +86,16 @@ testMintingDelegation = do expiresAt = now `addRelTime` days 60 reason = "Test development fund coupon" + let rewardCouponV2Amount = 42.0 + rewardCouponV2Cid <- submit app.dso $ createCmd RewardCouponV2 with + dso = app.dso + provider = provider2.primaryParty + amount = rewardCouponV2Amount + expiresAt = now `addRelTime` days 60 + round = openRound.round + providerIsObserver = False + beneficiary = Some beneficiary + -- Wait for rounds to advance so coupons can be minted runNextIssuance app runNextIssuance app @@ -106,7 +119,8 @@ testMintingDelegation = do InputAppRewardCoupon appRewardCouponCid, InputValidatorRewardCoupon validatorRewardCouponCid, InputUnclaimedActivityRecord unclaimedActivityRecordCid, - InputDevelopmentFundCoupon developmentFundCouponCid + InputDevelopmentFundCoupon developmentFundCouponCid, + InputRewardCouponV2 rewardCouponV2Cid ] context = PaymentTransferContext with context @@ -119,7 +133,7 @@ testMintingDelegation = do -- expected rewards based on issuance rates let expectedValidatorFaucetAmount = getIssuingMiningRoundIssuancePerValidatorFaucetCoupon issuingRound - let expectedAppReward = appRewardCouponAmount * issuingRound.issuancePerUnfeaturedAppRewardCoupon + let expectedAppReward = appRewardCouponAmount * issuingRound.issuancePerUnfeaturedAppRewardCoupon + rewardCouponV2Amount let expectedValidatorReward = validatorRewardCouponAmount * issuingRound.issuancePerValidatorRewardCoupon let expectedTotal = expectedValidatorFaucetAmount + expectedAppReward + expectedValidatorReward + unclaimedRewardCouponAmount + developmentFundCouponAmount @@ -256,3 +270,110 @@ testMintingDelegation_AcceptWithExistingDelegationArchival = do existingDelegationCid = Some mintingDelegationCid2 pure () + +testMintingDelegation_AssignAndMint : Script () +testMintingDelegation_AssignAndMint = do + DefaultAppWithUsers{..} <- setupDefaultAppWithUsers + + beneficiary <- allocateParty "beneficiary" + rewardCouponExtraBeneficiary <- allocateParty "rewardCouponExtraBeneficiary" + let delegate = aliceValidator.primaryParty + + -- Create a MintingDelegation + now <- getTime + proposalCid <- submit beneficiary $ createCmd MintingDelegationProposal with + delegation = MintingDelegation with + dso = app.dso + beneficiary + delegate + expiresAt = now `addRelTime` days 30 + amuletMergeLimit = 10 + + MintingDelegationProposal_AcceptResult mintingDelegationCid <- submit delegate $ exerciseCmd proposalCid MintingDelegationProposal_Accept with + existingDelegationCid = None + + -- Create a ValidatorRight for beneficiary (needed for liveness record) + validatorRightCid <- submit beneficiary $ createCmd ValidatorRight with + dso = app.dso + user = beneficiary + validator = beneficiary + + (_, openRound) <- getLatestOpenRound app + + -- Create a liveness record as an additional transfer input + livenessActivityRecordCid <- submit app.dso $ createCmd ValidatorLivenessActivityRecord with + dso = app.dso + validator = beneficiary + round = openRound.round + + -- Create two RewardCouponV2 with provider = beneficiary, beneficiary = None (unassigned) + let coupon1Amount = 1000.0 + coupon1Cid <- submit app.dso $ createCmd RewardCouponV2 with + dso = app.dso + provider = beneficiary + amount = coupon1Amount + expiresAt = now `addRelTime` hours 36 + round = openRound.round + providerIsObserver = True + beneficiary = None + + let coupon2Amount = 500.0 + coupon2Cid <- submit app.dso $ createCmd RewardCouponV2 with + dso = app.dso + provider = beneficiary + amount = coupon2Amount + expiresAt = now `addRelTime` hours 36 + round = openRound.round + providerIsObserver = True + beneficiary = None + + -- Advance rounds so coupons can be minted + runNextIssuance app + runNextIssuance app + runNextIssuance app + + balanceBefore <- getNormalizedBalance beneficiary + (openMiningRoundCid, _) <- getLatestOpenRound app + now2 <- getTime + issuingRounds <- query @IssuingMiningRound app.dso + let context = TransferContext with + openMiningRound = openMiningRoundCid + issuingMiningRounds = Map.fromList + [ (round.round, roundCid) | (roundCid, round) <- issuingRounds, round.opensAt <= now2 ] + validatorRights = Map.fromList [(beneficiary, validatorRightCid)] + featuredAppRight = None + (amuletRulesCid, _) <- getAmuletRules app + + -- Assign reward coupons to beneficiary and the rewardCouponExtraBeneficiary, and mint beneficiary's share + let weights = [(beneficiary, 0.6), (rewardCouponExtraBeneficiary, 0.4)] + submitMulti [delegate] [beneficiary, rewardCouponExtraBeneficiary, app.dso] $ exerciseCmd mintingDelegationCid MintingDelegation_AssignAndMint with + couponCid = toInterfaceContractId coupon1Cid + assignBeneficiaries = Api.RewardAssignmentV1.RewardCoupon_AssignBeneficiaries with + additionalCoupons = [toInterfaceContractId coupon2Cid] + newBeneficiaries = map (uncurry Api.RewardAssignmentV1.RewardBeneficiary) weights + extraArgs = emptyExtraArgs + inputs = [InputValidatorLivenessActivityRecord livenessActivityRecordCid] + context = PaymentTransferContext with + context + amuletRules = amuletRulesCid + + balanceAfter <- getNormalizedBalance beneficiary + let reward = balanceAfter - balanceBefore + + let Some (_, issuingRound) = head issuingRounds + + -- Expected: beneficiary gets 60% of coupon amounts + validator faucet reward + let expectedValidatorFaucetAmount = getIssuingMiningRoundIssuancePerValidatorFaucetCoupon issuingRound + let expectedRewardCouponAmount = (coupon1Amount + coupon2Amount) * 0.6 + let expectedTotal = expectedValidatorFaucetAmount + expectedRewardCouponAmount + + require ("Combined reward should match expected " <> show expectedTotal <> ", got: " <> show reward) + (reward >= expectedTotal - 1.0 && reward <= expectedTotal + 1.0) + + -- Verify rewardCouponExtraBeneficiary's coupons amounts + rewardCouponExtraBeneficiaryCoupons <- query @RewardCouponV2 rewardCouponExtraBeneficiary + let rewardCouponExtraBeneficiaryAmounts = map (\(_, c) -> c.amount) rewardCouponExtraBeneficiaryCoupons + require "rewardCouponExtraBeneficiary coupon amounts should be 40% of originals" + (sort rewardCouponExtraBeneficiaryAmounts == sort [coupon1Amount * 0.4, coupon2Amount * 0.4]) + + pure () diff --git a/daml/splice-wallet/daml.yaml b/daml/splice-wallet/daml.yaml index 0e0889c23d..e9d3a74f1b 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.18 +version: 0.1.19 dependencies: - daml-prim - daml-stdlib @@ -11,6 +11,7 @@ data-dependencies: - ../../token-standard/splice-api-token-transfer-instruction-v1/.daml/dist/splice-api-token-transfer-instruction-v1-current.dar - ../../token-standard/splice-api-token-allocation-instruction-v1/.daml/dist/splice-api-token-allocation-instruction-v1-current.dar - ../../token-standard/splice-api-token-allocation-v1/.daml/dist/splice-api-token-allocation-v1-current.dar +- ../splice-api-reward-assignment-v1/.daml/dist/splice-api-reward-assignment-v1-current.dar - ../splice-amulet/.daml/dist/splice-amulet-current.dar - ../splice-util/.daml/dist/splice-util-current.dar - ../splice-wallet-payments/.daml/dist/splice-wallet-payments-current.dar diff --git a/daml/splice-wallet/daml/Splice/Wallet/MintingDelegation.daml b/daml/splice-wallet/daml/Splice/Wallet/MintingDelegation.daml index e5970564a4..ce662ebf40 100644 --- a/daml/splice-wallet/daml/Splice/Wallet/MintingDelegation.daml +++ b/daml/splice-wallet/daml/Splice/Wallet/MintingDelegation.daml @@ -4,7 +4,9 @@ module Splice.Wallet.MintingDelegation where import DA.Assert +import Splice.Amulet (RewardCouponV2(..)) import Splice.AmuletRules +import Splice.Api.RewardAssignmentV1 qualified as Api.RewardAssignmentV1 import Splice.Types (ForOwner(..)) import Splice.Util @@ -76,6 +78,45 @@ template MintingDelegation expectedDso = Some dso pure MintingDelegation_MintResult with transferResult + nonconsuming choice MintingDelegation_AssignAndMint : MintingDelegation_AssignAndMintResult + with + couponCid : ContractId Api.RewardAssignmentV1.RewardCoupon + -- ^ Primary coupon to exercise assign on. + assignBeneficiaries : Api.RewardAssignmentV1.RewardCoupon_AssignBeneficiaries + -- ^ Assignment parameters: additionalCoupons, newBeneficiaries, extraArgs. + inputs : [TransferInput] + -- ^ Additional inputs to the transfer. + context : PaymentTransferContext + -- ^ Transfer context including amulet rules. + controller delegate + do + assertWithinDeadline "expiresAt" expiresAt + + -- Assign reward coupons to new beneficiaries + assignResult <- exercise couponCid assignBeneficiaries + + -- Find reward coupons assigned to the delegation's beneficiary and mint along with other inputs + let myRewardInputs = + [ InputRewardCouponV2 (fromInterfaceContractId @RewardCouponV2 cid) + | (party, cids) <- assignResult.newBeneficiariesCouponCids + , party == beneficiary + , cid <- cids + ] + + transferResult <- exercise context.amuletRules AmuletRules_Transfer with + transfer = Transfer with + sender = beneficiary + provider = delegate + inputs = inputs ++ myRewardInputs + outputs = [] + beneficiaries = None + context = context.context with featuredAppRight = None + expectedDso = Some dso + + pure MintingDelegation_AssignAndMintResult with + assignResult + transferResult + choice MintingDelegation_Reject : MintingDelegation_RejectResult controller delegate do pure MintingDelegation_RejectResult {} @@ -106,6 +147,12 @@ data MintingDelegation_MintResult = MintingDelegation_MintResult transferResult : TransferResult deriving (Show, Eq) +data MintingDelegation_AssignAndMintResult = MintingDelegation_AssignAndMintResult + with + assignResult : Api.RewardAssignmentV1.RewardCoupon_AssignBeneficiariesResult + transferResult : TransferResult + deriving (Show, Eq) + instance HasCheckedFetch MintingDelegation ForOwner where contractGroupId MintingDelegation{beneficiary, dso} = ForOwner with owner = beneficiary; dso diff --git a/daml/splitwell-test/daml.yaml b/daml/splitwell-test/daml.yaml index 8473a63783..7ef619ce3c 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.21 +version: 0.1.22 dependencies: - daml-prim - daml-stdlib diff --git a/daml/splitwell/daml.yaml b/daml/splitwell/daml.yaml index 98f90d4d0b..212f158602 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.18 +version: 0.1.19 dependencies: - daml-prim - daml-stdlib diff --git a/docs/api-templates/splice-api-reward-assignment-v1-index-template.rst b/docs/api-templates/splice-api-reward-assignment-v1-index-template.rst new file mode 100644 index 0000000000..73ee4dc429 --- /dev/null +++ b/docs/api-templates/splice-api-reward-assignment-v1-index-template.rst @@ -0,0 +1,13 @@ +.. + Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +.. + SPDX-License-Identifier: Apache-2.0 + +splice-api-reward-assignment-v1 docs +==================================== + +.. toctree:: + :maxdepth: 3 + :titlesonly: + +{{{body}}} diff --git a/docs/src/app_dev/daml_api/index.rst b/docs/src/app_dev/daml_api/index.rst index 9b7582fd42..84f8a1d3c5 100644 --- a/docs/src/app_dev/daml_api/index.rst +++ b/docs/src/app_dev/daml_api/index.rst @@ -29,6 +29,16 @@ Refer to the :ref:`Token Standard documentation section `. Featured App Activity Markers API (CIP-0047) -------------------------------------------- +.. important:: + + On networks where traffic-based app rewards as described in `CIP-0104 `__ are enabled, + the Featured App Activity Markers API will become irrelevant. + On such networks, the API is still supported, but no rewards can be earned using it. + We recommend apps to stop creating featured app activity markers once CIP-0104 is enabled. + + The documentation here is provided for apps that need to integrate with app rewards prior + to CIP-0104 being enabled to avoid unnecessary traffic costs. + * See the `text of the CIP-0047 `__ for its background on its design and its specification. diff --git a/docs/src/release_notes_upcoming.rst b/docs/src/release_notes_upcoming.rst index 02b7510cce..b7c8716fcc 100644 --- a/docs/src/release_notes_upcoming.rst +++ b/docs/src/release_notes_upcoming.rst @@ -54,3 +54,58 @@ - LocalNet - Added support for configuring the protocol version used in LocalNet. + + .. important:: + + **Action recommended for validator operators:** upgrade to this release + before the SVs start testing traffic-based app rewards in dry-run mode + (see `SV Longterm Operations Schedule `__ for dates for the different networks). + Otherwise, CC transfers and reward collection will stop working for parties on your node until you upgrade. + + **Action recommended for app devs:** app's with Daml code that statically depends on ``splice-amulet`` + should recompile their Daml code + to link against the new version of ``splice-amulet`` listed below. Otherwise, code involving CC transfers + will stop working as both ``OpenMiningRound`` and ``AmuletRules`` include newly introduced config fields. + + Apps that build against the :ref:`token_standard` API are not required to change except for upgrading + their validator node. + + - Daml + + - Add ``RewardCouponV2`` to represent rewards available from traffic-based app rewards that are computed + by the SV apps off-ledger as described in `CIP 104 `__. + They are created in an efficient batched fashion once per-round for every party that is eligible for traffic-based app rewards. + + In contrast to the existing reward coupons, these new coupons are using time based expiry, + and can be minted by default up to 36h after their creation. Thereby allowing their beneficiaries + to batch the minting to save traffic costs. + + They can be minted like all other coupon types using one of the following methods: + + 1. Automated minting via the Splice Wallet backend that is part of the validator app, + which works for onboarded internal parties and for external parties with a :ref:`minting delegation `. + 2. Direct minting by constructing calls to ``AmuletRules_Transfer`` that uses them as + an transfer input. These calls can be made directly against the Ledger API, or indirectly + via custom Daml code deployed to the validator node. + + - Add a new field ``rewardConfig`` to the ``AmuletConfig`` for configuring whether rounds should use + traffic-based app rewards or on-ledger reward accounting, and whether traffic-based app reward coupon creation + should be simulated in a dry-run mode. See the + :ref:`RewardConfig ` + data type definition for the list reward configuration fields and their semantics. + + - Store the current ``rewardConfig`` and ``trafficPrice`` on every ``OpenMiningRound`` contract when creating it. + This information serves to synchronize the SV apps on the parameters to use for processing traffic-based app rewards. + + - Add ``CalculateRewardsV2`` and ``ProcessRewardsV2`` templates together with supporting code + to implement the creation of the new reward coupons based on the reward + values computed off-ledger by the SV apps. + + - Adjust the CC transfer implementation such that it stops creating featured app activity markers + when it runs against a round (or external party configuration state) where traffic-based app rewards + are enabled. + Due to the propagation delay of updating the external party configuration state in the ``splice-amulet`` code, + there will be a transition phase where token standard CC transfers still create featured app markers. + These will be automatically archived as soon as traffic-based app rewards are enabled. + Thus no double-issuance of rewards will occur. + diff --git a/scripts/rename.sh b/scripts/rename.sh index ba23359fb9..46139572d9 100755 --- a/scripts/rename.sh +++ b/scripts/rename.sh @@ -1181,7 +1181,7 @@ function subcmd_no_illegal_daml_references() { done local illegal_patterns=( svc SVC Svc # to avoid conflict with PerSvContracts - '(? Decimal -> Script () -runNextIssuance dso amuletPrice = do +runNextIssuance : Party -> Decimal -> Optional Decimal -> Script () +runNextIssuance dso amuletPrice appActivityRoundTotal = do [(amuletRulesCid, _)] <- query @AmuletRules dso advanceToNextRoundChange dso -- expire rewards for closed rounds @@ -484,10 +485,29 @@ runNextIssuance dso amuletPrice = do validatorLivenessActivityRecords <- queryFilter @ValidatorLivenessActivityRecord dso (\bc -> bc.round == roundToArchive.round) svRewardCoupons <- queryFilter @SvRewardCoupon dso (\bc -> bc.round == roundToArchive.round) + Some summarizingRound <- queryContractId dso closingRoundCid + + let trafficBasedRewards = useTrafficBasedAppRewards ((.mintingVersion) <$> summarizingRound.rewardConfig) + + -- compute app reward totals: for traffic-based rewards, convert activity (MB) to Amulet + -- via trafficPrice ($/MB) and amuletPrice ($/Amulet); otherwise use on-ledger coupons + (totalFeaturedCoupons, totalUnfeaturedCoupons) <- + if trafficBasedRewards then + case (appActivityRoundTotal, summarizingRound.trafficPrice) of + (Some activity, Some trafficPrice) -> + pure (activity * trafficPrice / summarizingRound.amuletPrice, 0.0) + (None, _) -> + fail "appActivityRoundTotal must be specified when using traffic-based app rewards" + (_, None) -> + fail "trafficPrice must be specified when using traffic-based app rewards" + else + pure (sum [ c.amount | (_, c) <- appRewardCoupons, c.featured] + , sum [ c.amount | (_, c) <- appRewardCoupons, not (c.featured)]) + let summary = OpenMiningRoundSummary with totalValidatorRewardCoupons = sum [ c.amount | (_, c) <- validatorRewardCoupons] - totalFeaturedAppRewardCoupons = sum [ c.amount | (_, c) <- appRewardCoupons, c.featured] - totalUnfeaturedAppRewardCoupons = sum [ c.amount | (_, c) <- appRewardCoupons, not (c.featured)] + totalFeaturedAppRewardCoupons = totalFeaturedCoupons + totalUnfeaturedAppRewardCoupons = totalUnfeaturedCoupons totalSvRewardWeight = sum [ c.weight | (_, c) <- svRewardCoupons] optTotalValidatorFaucetCoupons = Some (length validatorFaucetCoupons + length validatorLivenessActivityRecords) diff --git a/token-standard/splice-token-standard-test/daml/Splice/Testing/Registries/AmuletRegistry/Parameters.daml b/token-standard/splice-token-standard-test/daml/Splice/Testing/Registries/AmuletRegistry/Parameters.daml index ee97d7eb8c..f52f5fcef5 100644 --- a/token-standard/splice-token-standard-test/daml/Splice/Testing/Registries/AmuletRegistry/Parameters.daml +++ b/token-standard/splice-token-standard-test/daml/Splice/Testing/Registries/AmuletRegistry/Parameters.daml @@ -91,6 +91,8 @@ defaultAmuletConfig = AmuletConfig with optDevelopmentFundManager = None + rewardConfig = None + -- | Default configuration schedule with single current amulet config defaultAmuletConfigSchedule : Schedule Time (AmuletConfig USD) defaultAmuletConfigSchedule = Schedule with @@ -164,5 +166,5 @@ defaultSynchronizerFeesConfig : SynchronizerFeesConfig defaultSynchronizerFeesConfig = SynchronizerFeesConfig with baseRateTrafficLimits = defaultBaseRateTrafficLimits minTopupAmount = 1_000_000 -- 1MB - extraTrafficPrice = 1.0 + extraTrafficPrice = 1.0 -- USD/MB readVsWriteScalingFactor = 4 -- charge 4 per 10,000, i.e., 0.04% of write cost for every read diff --git a/token-standard/splice-token-standard-test/daml/Splice/Tests/TestAmuletTokenTransfer.daml b/token-standard/splice-token-standard-test/daml/Splice/Tests/TestAmuletTokenTransfer.daml index cd907ee330..c145665aa3 100644 --- a/token-standard/splice-token-standard-test/daml/Splice/Tests/TestAmuletTokenTransfer.daml +++ b/token-standard/splice-token-standard-test/daml/Splice/Tests/TestAmuletTokenTransfer.daml @@ -192,9 +192,9 @@ test_happy_path_self = script do WalletClient.checkBalance bob registry.instrumentId 50.0 -- advance 3 times to make sure all previously active (even if not yet open) mining round contracts are closed - AmuletRegistry.runNextIssuance registry.dso 1.0 - AmuletRegistry.runNextIssuance registry.dso 1.0 - AmuletRegistry.runNextIssuance registry.dso 1.0 + AmuletRegistry.runNextIssuance registry.dso 1.0 None + AmuletRegistry.runNextIssuance registry.dso 1.0 None + AmuletRegistry.runNextIssuance registry.dso 1.0 None -- Trigger a self-transfer result <- submitWithDisclosures' bob enrichedChoice.disclosures $ exerciseCmd enrichedChoice.factoryCid enrichedChoice.arg