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"
+}
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