diff --git a/build.sbt b/build.sbt index 34761ff7ce..582166fe34 100644 --- a/build.sbt +++ b/build.sbt @@ -117,6 +117,7 @@ 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-token-metadata-v1-daml`, `splice-api-token-holding-v1-daml`, `splice-api-token-transfer-instruction-v1-daml`, @@ -663,7 +664,7 @@ lazy val `splice-util-daml` = `canton-bindings-java` ) -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) @@ -674,7 +675,7 @@ lazy val `splice-featured-app-api-v1-daml` = `canton-bindings-java` ) -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) @@ -699,8 +700,8 @@ 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-featured-app-v1-daml` / Compile / damlBuild).value ++ + (`splice-api-featured-app-v2-daml` / Compile / damlBuild).value, ) .dependsOn(`canton-bindings-java`) @@ -811,8 +812,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, ) .dependsOn(`canton-bindings-java`) @@ -826,8 +827,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, ) .dependsOn(`canton-bindings-java`) @@ -864,8 +865,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, ) .dependsOn(`canton-bindings-java`) @@ -963,8 +964,8 @@ 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-util-batched-markers-daml`, ) .enablePlugins(BuildInfoPlugin) diff --git a/daml/dars.lock b/daml/dars.lock index 375485ea9c..ecbfc223c4 100644 --- a/daml/dars.lock +++ b/daml/dars.lock @@ -7,7 +7,7 @@ splice-amulet 0.1.13 6e9fc50fb94e56751b49f09ba2dc84da53a9d7cff08115ebb4f6b7a12d0 splice-amulet 0.1.14 3ca1343ab26b453d38c8adb70dca5f1ead8440c42b59b68f070786955cbf9ec1 splice-amulet 0.1.15 67fac2f853bce8dbf0b9817bb5ba7c59f10e8120b7c808696f7010e5f0c8a791 splice-amulet 0.1.16 c208d7ead1e4e9b610fc2054d0bf00716144ad444011bce0b02dcd6cd0cb8a23 -splice-amulet 0.1.17 5450b9117e55d473f37a86daad480856ed4f59d639485057f046bf0f0254752b +splice-amulet 0.1.17 51ea72cf66796899b22469cc1f0a39809a1957d14439608e325712f9ead21556 splice-amulet 0.1.2 1446ffdf23326cef2de97923df96618eb615792bea36cf1431f03639448f1645 splice-amulet 0.1.3 0d89016d5a90eb8bced48bbac99e81c57781b3a36094b8d48b8e4389851e19af splice-amulet 0.1.4 a36ef8888fb44caae13d96341ce1fabd84fc9e2e7b209bbc3caabb48b6be1668 @@ -26,7 +26,7 @@ splice-amulet-name-service 0.1.14 6cb1318176e758c256c2e385f87b86c5060e80fb68a72e splice-amulet-name-service 0.1.15 d4724b90dce9fb08badbb367962d237710b3a603e4f57806a1b0af308cc70fdb splice-amulet-name-service 0.1.16 53468a38bce11b51cd2ed10b9c09301c0b73570b50896d5649c4629de15815a3 splice-amulet-name-service 0.1.17 bcc80dce253c7b89efd9b263be5260a9609f8cb1fb5ea6e9916f6904552bdc82 -splice-amulet-name-service 0.1.18 238c0a20346fcb10a3c87249ee916239ae5d4b52f874fa2042aac9d0a65f3c1a +splice-amulet-name-service 0.1.18 4987b8db10462a94b3e672ef18437b39d2f8d359c02cae57e82819406d8947ef 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 +35,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 2edfe0f32f414b2e4d211cc74d7fefe5b40dd5930dde58fe7339b5b504259058 -splice-amulet-test 0.1.20 800087d5759849775e4af6e6149a95c0506521c7c785257583f80ad5019ae284 +splice-amulet-name-service-test 0.1.21 254bdd2a6172b0adc4acec920981f236db9baa7a361a87d8f2e88e1731470d99 +splice-amulet-test 0.1.20 d1e44bcdc8ac0c8189eff9bf3cce8ef6badc846dd82f2489c6cac9199db18114 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 @@ -64,7 +65,7 @@ splice-dso-governance 0.1.2 4206e127be8b111ac84bd7f98bd9dbf03ed489f1642b46ab31a4 splice-dso-governance 0.1.20 996a3b619d6b65ca7812881978c44c650cac119de78f5317d1f317658943001c splice-dso-governance 0.1.21 2d306cfe8cdb3daf2d21f84dfecc3e2f26a41504e58fe25cb7fe5cc65683d11f splice-dso-governance 0.1.22 5c28530209b9ab37c5f187132cd826709bb18b0efe28411488ab750870414738 -splice-dso-governance 0.1.23 1d30f443af2ee80a58b266362e6212a6f848ef1ac79affec0626ac84c0f54ee1 +splice-dso-governance 0.1.23 c2263b38e1eed13a2deccf76772ddf1b8248f9bce18275d4e0310808097982c1 splice-dso-governance 0.1.3 b0ae3cc03e418790305a3c15f761fe495572de5827f8d322fb8b96996b783c13 splice-dso-governance 0.1.4 dc24fd18b4d151cd1e0ff6bfb7438bafb2f50fe076d0f16f50565e60b153a0be splice-dso-governance 0.1.5 9e3ca1d22ad495dfabf3d61acae3dc1a7718f527f02092280b58cf69edfdc84c @@ -72,8 +73,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.27 7e518a0382974e926d8e1e1b1ef1073958c8b0b5a0ae026c0c6286ae02facd5d -splice-token-standard-test 1.0.11 f227323caf712d2778fe39b3393801032bfc2db8804dd4d1cdd0e54a85a7ce50 +splice-dso-governance-test 0.1.27 1e865708d2ff84ffdfd1b053e328260a82a30c43f1813d2469a0902905b0067e +splice-token-standard-test 1.0.11 685c3307bee61b734e2bda5746a2d21907254527f4d56f8257074e0b4ef40730 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 @@ -85,15 +86,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 4e252a71e4b3697dad8f94338e1707375df04bc71488b4de867ee034bee3af5f +splice-util-batched-markers-test 1.0.3 84d63b6113e37f76e8f5cc2cd0501c57082a8368cce7a7156f82a40600af616d 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 914c27e2fde975f05bc7a3745092d4fb4966ca9b0eb4ed434c744ab8b9d505d3 +splice-util-featured-app-proxies-test 1.0.9 78e025a0e464461570b98883261cf4865c0f2d4316d87268a374843fc88bd24d splice-util-token-standard-wallet 1.0.0 1da198cb7968fa478cfa12aba9fdf128a63a8af6ab284ea6be238cf92a3733ac -splice-util-token-standard-wallet-test 1.0.4 4b83254842725f4ff5882d8b45fc86b5e74b6ccdf324e0c8b34683555f23c133 +splice-util-token-standard-wallet-test 1.0.4 58c7e31a0490cc770771d93ddd3ea72c10b7b16dfdd2ad3ae72b886cf7108b3f splice-validator-lifecycle 0.1.0 cef96fac957362f1fc097120bd13686cac7f84fbc8053afa994a1f9214d9570c splice-validator-lifecycle 0.1.1 1ddf05c96002914593c929848b786f34c753fb0be07717d1786be177a564aada splice-validator-lifecycle 0.1.2 57e2f15f9755db1f00e51c52c319294264a21ad71c6bc1e7cd70db4b164c0aaa @@ -112,7 +113,7 @@ splice-wallet 0.1.14 690c1d47bac06db419db344d59a7a30c53fa3f5d961943fe1782cfc6c78 splice-wallet 0.1.15 fd57252dda29e3ce90028114c91b521cb661df5a9d6e87c41a9e91518215fa5b splice-wallet 0.1.16 17dca10fd8eb6a833be530fe9c6f9c2b7397a38c06e9c86d0679adc200b90e14 splice-wallet 0.1.17 176c2924cd7aa12bc81ffd1a8d6cfaf46e70378f653eb5f19f2d6b9599cfd45c -splice-wallet 0.1.18 4c613442f4e3be8ab230a4ea58773e26d0c2e52521bceb081ee6bbed1f1c1a80 +splice-wallet 0.1.18 c1619906ba50bcaf13cd506fa3be324e702791fee791a4d5872a41804da9829f splice-wallet 0.1.2 c162e08a4ec0428bfa870b6d9040989e575c74199c3a80558c62e03196dd5146 splice-wallet 0.1.3 2c35bb4f5084ea66db59717d21750bfd64c43147ef5fd5166615092d592a6917 splice-wallet 0.1.4 141dad2d33b6410b8e1c35a0c4f8f76cb691e4d9a4410ce89f33f373855317e1 @@ -130,7 +131,7 @@ splice-wallet-payments 0.1.13 0b9250642d3864e6bbea553264dcac0d286104f24efad2fbaf splice-wallet-payments 0.1.14 45b29d6e05b5352c39edde850c66b4535c682b9991b06eec312176b1a48ecab5 splice-wallet-payments 0.1.15 f80fae7a9de9431854372a66c3ca78675f77b2f54ede65abdc1b1abdec707d21 splice-wallet-payments 0.1.16 45e7ac4601186747e2c4d2fd7e54a15e5752eee56d6cf767eb62141b7a10c0a8 -splice-wallet-payments 0.1.17 323e71d389f00edb009a99c804d9bd460b497784b9be94706d2db18e42ec4004 +splice-wallet-payments 0.1.17 985e1450aaab1d9accbc688c639fb4467164c9ac36274fb7d2d01cf2a63fc11c splice-wallet-payments 0.1.2 775f5eb9c0249509adda5eb3ea4ee31bb953601168c18880df6f2ff09ec4298a splice-wallet-payments 0.1.3 b953b3729c81a55e598a364be7d0c0574750df3de12a7a1b53a300f217cb5c5c splice-wallet-payments 0.1.4 12177f54873c1094ea169874ad0d7838383fd137f302d16356e93f28dfbc0fcc @@ -139,7 +140,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 9479191afa33272c3c24c09a6663d12b31a9bbf9866435e9bd09e3e640b4429f +splice-wallet-test 0.1.21 251d7245feedc99ede824a42decc29bd360e7b9128900280e2fc4d0f2fb82e85 splitwell 0.1.0 075c76de553ab88383a7c69de134afa82aacfdf8ea8fcfe8852c4b199c3b2669 splitwell 0.1.1 ccb1a0215053062202052e1a052f9214da3fdae5253a6d43e2e155ff4f57fe75 splitwell 0.1.10 d42676a366f7ca7a2409974dd3054aa4d83ab29baa3b2086ad021407b0a1a295 @@ -150,7 +151,7 @@ splitwell 0.1.14 bf2ec3fec9bcb58ed5e2ff63072a1e4994d0415ea7a0275942be282906a4202 splitwell 0.1.15 2f3d8a50f57e66af450c36556a09d04c1d9117b699720118b7bd302556805499 splitwell 0.1.16 2c8567bc0e7cd15d29de8dcbc8d992aa7a42a3805e9831765d670b03c7c2474a splitwell 0.1.17 a631654e66ef31017bf3c9cb4ab2429157d5e5f948f1b6b15a38f0ec7c0cd363 -splitwell 0.1.18 008b9222e2b638f35c1d24d1d54503314666b72a6fd84a4da6c6d85dd87233f5 +splitwell 0.1.18 b13f73a697b31c92e7e648417a8cee22bf1b374d4fb71850304dd84fe0bf6ed5 splitwell 0.1.2 778edd2c228c6b68198d4d033885b2d0dae7daaee55d7df3edd9dfdf1f10fbd0 splitwell 0.1.3 7cde068cde689584f86a2499689d5cb165264d96496721e24ac6fb909f770a58 splitwell 0.1.4 85557b86cd4f330f093915db1ea26eac5092de6b5ddae0690146f6059c89419b @@ -159,4 +160,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 91e587e2d9b7659ad55bb841101a96b8c1681fdaebee63e09b9241219288cbc0 \ No newline at end of file +splitwell-test 0.1.21 3ab22caea0e8deef6f3f19df3b5beba76f297de9fdac079184eef7d3788bcdf7 \ No newline at end of file diff --git a/daml/dars/splice-amulet-0.1.17.dar b/daml/dars/splice-amulet-0.1.17.dar index cf27d85c20..09a455bf2c 100644 Binary files a/daml/dars/splice-amulet-0.1.17.dar and b/daml/dars/splice-amulet-0.1.17.dar differ diff --git a/daml/dars/splice-amulet-name-service-0.1.18.dar b/daml/dars/splice-amulet-name-service-0.1.18.dar index 8c2436075d..26af5563f2 100644 Binary files a/daml/dars/splice-amulet-name-service-0.1.18.dar and b/daml/dars/splice-amulet-name-service-0.1.18.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.23.dar b/daml/dars/splice-dso-governance-0.1.23.dar index 00ceea0beb..e60928541d 100644 Binary files a/daml/dars/splice-dso-governance-0.1.23.dar and b/daml/dars/splice-dso-governance-0.1.23.dar differ diff --git a/daml/dars/splice-wallet-0.1.18.dar b/daml/dars/splice-wallet-0.1.18.dar index 9899204d7f..67059da1de 100644 Binary files a/daml/dars/splice-wallet-0.1.18.dar and b/daml/dars/splice-wallet-0.1.18.dar differ diff --git a/daml/dars/splice-wallet-payments-0.1.17.dar b/daml/dars/splice-wallet-payments-0.1.17.dar index 8c24338ada..5808191288 100644 Binary files a/daml/dars/splice-wallet-payments-0.1.17.dar and b/daml/dars/splice-wallet-payments-0.1.17.dar differ diff --git a/daml/dars/splitwell-0.1.18.dar b/daml/dars/splitwell-0.1.18.dar index 48be12f460..113fa98974 100644 Binary files a/daml/dars/splitwell-0.1.18.dar and b/daml/dars/splitwell-0.1.18.dar differ 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/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..7f46b36dba --- /dev/null +++ b/daml/splice-amulet-test/daml/Splice/Scripts/TestRewardAccountingV2.daml @@ -0,0 +1,481 @@ +-- 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.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 + + +-- 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 + + -- 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 + runNextIssuance app + + -- 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 + beneficiary = provider + amount + round = Round 0 + expiresAt = couponExpiryTime + beneficiaryIsObserver = provider `notElem` [bob, dora] + + actualCoupons0 <- query @RewardCouponV2 app.dso + let actualCoupons = sortOn (.beneficiary) $ 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.beneficiaryIsObserver) + void $ submit app.dso $ exerciseCmd amuletRulesCid AmuletRules_UnhideRewardCouponsV2 with + rewardCouponCids = map fst unobservableCoupons + beneficiaries = map (._2.beneficiary) unobservableCoupons + + couponsAfterUnhiding <- query @RewardCouponV2 app.dso + sortOn (.beneficiary) (map snd couponsAfterUnhiding) === + map (\c -> c with beneficiaryIsObserver = 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 + runNextIssuance app + + -- 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 + setTime demoTime + aliceUser <- setupUserWithoutValidatorRight app "Alice" + let alice = aliceUser.primaryParty + + -- bare create a coupon + let coupon = RewardCouponV2 with + dso = app.dso + provider = alice + beneficiary = alice + amount = 1000.0 + round = Round 0 + expiresAt = demoTime `addRelTime` hours 36 + beneficiaryIsObserver = not config.hideCoupon + 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 + + +-- 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 + beneficiary = bob + amount = 500.0 + round = Round 0 + expiresAt = demoTime `addRelTime` hours 48 + beneficiaryIsObserver = False + 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 + + -- 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 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 + 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 + + -- 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 + + 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..d39c289234 --- /dev/null +++ b/daml/splice-amulet-test/daml/Splice/Scripts/UnitTests/Amulet/CryptoHash.daml @@ -0,0 +1,368 @@ +-- Copyright (c) 2026 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..beadf95cc6 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 -------------------------- @@ -312,7 +331,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 +339,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 +435,9 @@ runAmuletDepositBots app = do svRewardCoupons <- queryFilter @SvRewardCoupon app.dso $ \c -> c.round `elem` issuingRoundNumbers && c.beneficiary == user + rewardCouponV2s <- queryFilter @RewardCouponV2 app.dso $ \c -> + 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 +454,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 +471,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 +670,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/Splice/Amulet.daml b/daml/splice-amulet/daml/Splice/Amulet.daml index fcb8f067f1..f54acee062 100644 --- a/daml/splice-amulet/daml/Splice/Amulet.daml +++ b/daml/splice-amulet/daml/Splice/Amulet.daml @@ -267,38 +267,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 +371,30 @@ 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. + beneficiary : Party -- ^ The party that can mint the reward for the activity by the provider. + 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. + beneficiaryIsObserver : Bool + -- ^ Whether the beneficiary 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 beneficiary an observer when they + -- change their vetting state unless the coupon expired in the meantime. + where + signatory dso + observer if beneficiaryIsObserver then [beneficiary] else [] + ensure amount > 0.0 + -- | A coupon for receiving validator rewards proportional to the usage fee paid by a user -- hosted by a validator operator. @@ -537,6 +560,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,7 +572,6 @@ requireAmuletExpiredForAllRounds externalPartyConfigState0Cid externalPartyConfi require "Amulet is expired" (isAmuletExpired round amulet.amount) - -- instances ------------ @@ -559,6 +584,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 = 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..269b5758f0 --- /dev/null +++ b/daml/splice-amulet/daml/Splice/Amulet/CryptoHash.daml @@ -0,0 +1,116 @@ +-- Copyright (c) 2026 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..b6399958ce --- /dev/null +++ b/daml/splice-amulet/daml/Splice/Amulet/RewardAccountingV2.daml @@ -0,0 +1,129 @@ +-- Copyright (c) 2026 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 + beneficiary = provider + amount + expiresAt + beneficiaryIsObserver = + not $ Set.member provider providersWithWrongVettingState + + -- 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 cb77ed4e18..e6ec8f03a6 100644 --- a/daml/splice-amulet/daml/Splice/AmuletConfig.daml +++ b/daml/splice-amulet/daml/Splice/AmuletConfig.daml @@ -47,6 +47,39 @@ 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. + deriving (Eq, Show) + + +-- Full AmuletConfig +-------------------- + -- | Configuration includes TransferConfig, issuance curve and tickDuration -- -- See Splice.Scripts.Parameters for concrete values. @@ -63,8 +96,10 @@ data AmuletConfig unit = AmuletConfig with featuredAppActivityMarkerAmount : Optional Decimal -- ^ $-amount used for the conversion from FeaturedAppActivityMarker -> AppRewardCoupon optDevelopmentFundManager : Optional Party -- ^ Party authorized to manage and allocate minting rights from the Development Fund. - externalPartyConfigStateTickDuration : Optional RelTime + 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 @@ -136,6 +171,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 @@ -147,6 +185,14 @@ 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 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 cb65000336..3e986dea75 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 @@ -82,6 +88,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 @@ -382,6 +398,8 @@ template AmuletRules now <- getTime let configUsd = getValueAsOf now configSchedule let tickDuration = configUsd.tickDuration + let trafficPrice = Some configUsd.decentralizedSynchronizer.fees.extraTrafficPrice + let rewardConfig = configUsd.rewardConfig let nr0 = fromOptional 0 initialRound let nr1 = nr0 + 1 let nr2 = nr1 + 1 @@ -411,10 +429,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_UpdateToLatestSchemaVersion with openMiningRoundTriple = OpenMiningRoundTriple with @@ -445,19 +466,38 @@ 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 + + -- 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) @@ -471,6 +511,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 @@ -610,6 +655,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 = [ 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 "beneficiary is no observer on the coupon" (not (coupon.beneficiaryIsObserver)) + assertWithinDeadline "coupon.expiresAt" coupon.expiresAt + create coupon with beneficiaryIsObserver = True + pure coupon.beneficiary + + -- 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 @@ -748,27 +871,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_UpdateExternalPartyConfigStates : AmuletRules_UpdateExternalPartyConfigStatesResult with @@ -793,8 +923,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 @@ -820,6 +952,9 @@ validateOpenMiningRoundTriple dso roundTriple = do oldestRound = round0 latestUsableRound = maximumOn (.round.number) usableRounds +miningRoundTripleCids : OpenMiningRoundTriple -> [ContractId OpenMiningRound] +miningRoundTripleCids OpenMiningRoundTriple {..} = [round0Cid, round1Cid, round2Cid] + -- Transfer logic -- ============== @@ -938,6 +1073,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 @@ -997,6 +1133,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 @@ -1013,10 +1150,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 = @@ -1178,6 +1316,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 @@ -1284,22 +1439,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 @@ -1362,12 +1519,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) @@ -1473,6 +1631,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. @@ -1920,6 +2079,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 4662ddfb39..2ebc68bab2 100644 --- a/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml +++ b/daml/splice-amulet/daml/Splice/ExternalPartyAmuletRules.daml @@ -21,10 +21,9 @@ import Splice.AmuletRules import Splice.Types import Splice.Util - - import DA.Exception + -- | Rules contract that can be used in transactions that require signatures -- from an external party. This is intended to get archived and recreated as rarely as possible -- to support long delays between preparing and signing a transaction. diff --git a/daml/splice-amulet/daml/Splice/ExternalPartyConfigState.daml b/daml/splice-amulet/daml/Splice/ExternalPartyConfigState.daml index 3a57877bd0..fed32971e1 100644 --- a/daml/splice-amulet/daml/Splice/ExternalPartyConfigState.daml +++ b/daml/splice-amulet/daml/Splice/ExternalPartyConfigState.daml @@ -18,9 +18,10 @@ template ExternalPartyConfigState with dso : Party holdingFeesOpenRoundNumber : Round -- ^ The round number to use for charging holding fees for newly created contracts. - amuletPrice : Decimal -- ^ Amulet price at the time the config state was created. + 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 but the SVs will archive it shortly after this time has been reached. + 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..322542ba69 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 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..b643041b6c --- /dev/null +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestRewardAccountingV2.daml @@ -0,0 +1,213 @@ +-- 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 + + (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 + runNextIssuanceD app 1.0 + + -- 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 + beneficiary = provider + amount + round = Round 0 + expiresAt = couponExpiryTime + beneficiaryIsObserver = provider `notElem` [bob, dora] + + actualCoupons0 <- query @RewardCouponV2 app.dso + let actualCoupons = sortOn (.beneficiary) $ 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.beneficiaryIsObserver) + void $ submitMulti [sv1] [app.dso] $ exerciseCmd dsoRulesCid DsoRules_UnhideRewardCouponsV2 with + sv = sv1 + amuletRulesCid + choiceArg = AmuletRules_UnhideRewardCouponsV2 with + rewardCouponCids = map fst unobservableCoupons + beneficiaries = map (._2.beneficiary) unobservableCoupons + + couponsAfterUnhiding <- query @RewardCouponV2 app.dso + sortOn (.beneficiary) (map snd couponsAfterUnhiding) === + map (\c -> c with beneficiaryIsObserver = 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, alice, 100.0), (alice, bob, 500.0) ] $ \(provider, beneficiary, amount) -> do + submit app.dso $ createCmd RewardCouponV2 with + dso = app.dso + provider + beneficiary + amount + round = Round 0 + expiresAt = demoTime `addRelTime` hours 48 + beneficiaryIsObserver = False + + -- 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 d65efb7a78..c3c223c695 100644 --- a/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml +++ b/daml/splice-dso-governance-test/daml/Splice/Scripts/DsoTestUtils.daml @@ -127,11 +127,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 @@ -298,6 +294,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/daml/Splice/DsoRules.daml b/daml/splice-dso-governance/daml/Splice/DsoRules.daml index 7c80e618f8..5e049b3e0c 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 @@ -92,6 +93,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 @@ -261,6 +264,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 @@ -1307,6 +1325,8 @@ template DsoRules with sv : Optional Party controller sv do + _ <- getAndValidateSvParty this sv + -- TODO(#2025) Deprecate AmuletRules_ClaimExpiredRewards. We did not do that in the same change -- as inlining the definition here so don't need to bump splice-amulet which also requires validators to upgrade. let AmuletRules_ClaimExpiredRewards{..} = choiceArg @@ -1360,6 +1380,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 @@ -1638,6 +1708,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 @@ -1704,6 +1775,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-wallet-test/daml/Splice/Scripts/Wallet/TestMintingDelegation.daml b/daml/splice-wallet-test/daml/Splice/Scripts/Wallet/TestMintingDelegation.daml index 1e739ea8ab..d7180a204c 100644 --- a/daml/splice-wallet-test/daml/Splice/Scripts/Wallet/TestMintingDelegation.daml +++ b/daml/splice-wallet-test/daml/Splice/Scripts/Wallet/TestMintingDelegation.daml @@ -83,6 +83,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 + beneficiary = beneficiary + amount = rewardCouponV2Amount + expiresAt = now `addRelTime` days 60 + round = openRound.round + beneficiaryIsObserver = False + -- Wait for rounds to advance so coupons can be minted runNextIssuance app runNextIssuance app @@ -106,7 +116,8 @@ testMintingDelegation = do InputAppRewardCoupon appRewardCouponCid, InputValidatorRewardCoupon validatorRewardCouponCid, InputUnclaimedActivityRecord unclaimedActivityRecordCid, - InputDevelopmentFundCoupon developmentFundCouponCid + InputDevelopmentFundCoupon developmentFundCouponCid, + InputRewardCouponV2 rewardCouponV2Cid ] context = PaymentTransferContext with context @@ -119,7 +130,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 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.rst b/docs/src/release_notes.rst index c6a26dbe9a..9c99704a77 100644 --- a/docs/src/release_notes.rst +++ b/docs/src/release_notes.rst @@ -187,7 +187,7 @@ In some cases, helm might not properly update the state after you removed the `migrating` flag. You can check before the upgrade if it got properly applied through ``kubectl describe deployment -n validator validator-app`` and look for this env var: - .. code-block:: yaml + .. code-block:: yaml - name: ADDITIONAL_CONFIG_VALIDATOR_MIGRATION_RESTORE value: | diff --git a/docs/src/release_notes_upcoming.rst b/docs/src/release_notes_upcoming.rst index c7c8bc8865..ac778a75ce 100644 --- a/docs/src/release_notes_upcoming.rst +++ b/docs/src/release_notes_upcoming.rst @@ -73,3 +73,62 @@ `. - FIXME: Add versions required for this change + + .. 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. + + + - FIXME: Add Daml versions implementing the CIP-104 change + + 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 - '(?