From d6b6c163e9c7d30def973fe429ec6105182d556c Mon Sep 17 00:00:00 2001 From: canton-network-da Date: Thu, 3 Oct 2024 04:00:22 -0400 Subject: [PATCH] Update Splice from CCI (#52) Signed-off-by: DA Automation Co-authored-by: DA Automation --- ...ScanFrontendTimeBasedIntegrationTest.scala | 10 +- .../tests/SvFrontendIntegrationTest.scala | 33 +- .../tests/VotesFrontendTestUtil.scala | 12 + ...unbookSvPreflightIntegrationTestBase.scala | 3 +- .../frontend-test-utils/src/configDiffs.ts | 30 + apps/common/frontend-test-utils/src/index.ts | 6 +- .../src/mocks/handlers/dso-info-handler.ts | 123 ++-- apps/common/frontend/package.json | 7 +- .../frontend/src/__tests__/mocks/constants.ts | 219 ++++-- .../src/__tests__/votes/votes.test.tsx | 26 +- .../frontend/src/components/AccordionList.tsx | 88 +++ .../src/components/PrettyJsonDiff.tsx | 214 ++++++ apps/common/frontend/src/components/index.ts | 2 + .../src/components/votes/ActionView.tsx | 568 +++++++++++++-- .../src/components/votes/ListVoteRequests.tsx | 105 ++- .../src/components/votes/VoteModalContent.tsx | 13 +- .../votes/VoteRequestFilterTable.tsx | 13 +- .../votes/VoteRequestModalContent.tsx | 14 +- .../votes/VoteResultModalContent.tsx | 35 +- .../votes/VoteResultsFilterTable.tsx | 23 +- .../components/votes/VotesHooksProvider.tsx | 2 +- .../frontend/src/components/votes/index.ts | 1 + .../daml/network/http/HttpVotesHandler.scala | 19 +- apps/package-lock.json | 208 +++++- apps/sv/frontend/src/App.tsx | 9 +- .../src/__tests__/config-diffs.test.tsx | 208 ++++++ .../frontend/src/__tests__/mocks/constants.ts | 664 ++++++++++++++---- .../src/__tests__/mocks/handlers/sv-api.ts | 27 +- apps/sv/frontend/src/__tests__/sv.test.tsx | 1 - .../components/votes/SvListVoteRequests.tsx | 15 +- .../src/components/votes/VoteRequest.tsx | 88 ++- .../actions/AddFutureAmuletConfigSchedule.tsx | 10 +- .../src/contexts/SvAppVotesHooksContext.tsx | 4 +- .../src/hooks/useListVoteRequests.tsx | 6 +- build.sbt | 8 +- 35 files changed, 2308 insertions(+), 506 deletions(-) create mode 100644 apps/common/frontend-test-utils/src/configDiffs.ts create mode 100644 apps/common/frontend/src/components/AccordionList.tsx create mode 100644 apps/common/frontend/src/components/PrettyJsonDiff.tsx create mode 100644 apps/sv/frontend/src/__tests__/config-diffs.test.tsx diff --git a/apps/app/src/test/scala/com/daml/network/integration/tests/ScanFrontendTimeBasedIntegrationTest.scala b/apps/app/src/test/scala/com/daml/network/integration/tests/ScanFrontendTimeBasedIntegrationTest.scala index 6b7efbc2..a736ddfc 100644 --- a/apps/app/src/test/scala/com/daml/network/integration/tests/ScanFrontendTimeBasedIntegrationTest.scala +++ b/apps/app/src/test/scala/com/daml/network/integration/tests/ScanFrontendTimeBasedIntegrationTest.scala @@ -12,6 +12,7 @@ import com.daml.network.validator.automation.ReceiveFaucetCouponTrigger import com.digitalasset.canton.config.NonNegativeFiniteDuration import com.digitalasset.canton.integration.BaseEnvironmentDefinition import io.circe.JsonObject +import org.openqa.selenium.By import spray.json.DefaultJsonProtocol.StringJsonFormat import java.time.{Duration, Instant} @@ -580,14 +581,19 @@ class ScanFrontendTimeBasedIntegrationTest closeVoteModalsIfOpen reviewButton.underlying.click() - inside(find(id("pretty-json"))) { case Some(json) => + // TODO(#14813): needs to be changed by using parseAmuletConfigValue() once the diff exists for the first change + try { + val newScheduleItem = webDriver.findElement(By.id("accordion-details")) + val json = newScheduleItem.findElement(By.tagName("pre")).getText spray.json - .JsonParser(json.text) + .JsonParser(json) .asJsObject("transferConfig") .fields("transferConfig") .asJsObject .fields("maxNumInputs") .convertTo[String] should be(newMaxNumInputs.toString) + } catch { + case _: NoSuchElementException => false } } }, diff --git a/apps/app/src/test/scala/com/daml/network/integration/tests/SvFrontendIntegrationTest.scala b/apps/app/src/test/scala/com/daml/network/integration/tests/SvFrontendIntegrationTest.scala index 1b6924cd..2e393d49 100644 --- a/apps/app/src/test/scala/com/daml/network/integration/tests/SvFrontendIntegrationTest.scala +++ b/apps/app/src/test/scala/com/daml/network/integration/tests/SvFrontendIntegrationTest.scala @@ -16,14 +16,9 @@ import java.time.format.DateTimeFormatter import java.time.{LocalDateTime, ZoneOffset} import scala.concurrent.duration.DurationInt import scala.jdk.CollectionConverters.* -import com.daml.ledger.javaapi.data.codegen.json.JsonLfReader -import com.daml.network.codegen.java.splice.amuletconfig.AmuletConfig import com.daml.network.codegen.java.splice.dsorules.voterequestoutcome.VRO_AcceptedButActionFailed -import com.daml.network.codegen.java.splice.wallet.payment.Unit as DamlUnit import com.daml.network.sv.automation.delegatebased.CloseVoteRequestTrigger -import java.util.Optional - class SvFrontendIntegrationTest extends SvFrontendCommonIntegrationTest with SvTestUtil @@ -1008,25 +1003,15 @@ class SvFrontendIntegrationTest tb.text } - inside(find(id("pretty-json"))) { case Some(json) => - val amuletConfig = - AmuletConfig - .jsonDecoder(DamlUnit.jsonDecoder()) - .decode(new JsonLfReader(json.text)) - BigDecimal(amuletConfig.transferConfig.createFee.fee) should be( - BigDecimal(requestNewTransferConfigFeeValue) - ) - amuletConfig.issuanceCurve.initialValue.optValidatorFaucetCap should be( - Optional.empty - ) - amuletConfig.issuanceCurve.futureValues - .get(0) - ._2 - .optValidatorFaucetCap - .map(BigDecimal(_)) should be( - Optional.of(BigDecimal(optValidatorFaucetValue)) - ) - } + BigDecimal(parseAmuletConfigValue("createFee")) should be( + BigDecimal(requestNewTransferConfigFeeValue) + ) + BigDecimal(parseAmuletConfigValue("optValidatorFaucetCap", false)) should be( + BigDecimal("2.85") + ) + BigDecimal(parseAmuletConfigValue("optValidatorFaucetCap")) should be( + BigDecimal(optValidatorFaucetValue) + ) requestId }, diff --git a/apps/app/src/test/scala/com/daml/network/integration/tests/VotesFrontendTestUtil.scala b/apps/app/src/test/scala/com/daml/network/integration/tests/VotesFrontendTestUtil.scala index 444c2be5..0ef23d82 100644 --- a/apps/app/src/test/scala/com/daml/network/integration/tests/VotesFrontendTestUtil.scala +++ b/apps/app/src/test/scala/com/daml/network/integration/tests/VotesFrontendTestUtil.scala @@ -28,4 +28,16 @@ trait VotesFrontendTestUtil { self: FrontendIntegrationTestWithSharedEnvironment scala.util.Try(click on "vote-result-modal-close-button") } + def parseAmuletConfigValue(key: String, replacement: Boolean = true)(implicit + webDriver: WebDriverType + ) = { + val headElement = webDriver.findElement(By.cssSelector(s"li[data-key='$key']")) + val value = if (replacement) { + headElement.findElement(By.cssSelector("div.jsondiffpatch-right-value")) + } else { + headElement.findElement(By.cssSelector("div.jsondiffpatch-left-value")) + } + value.findElement(By.tagName("pre")).getText.replace("\"", "") + } + } diff --git a/apps/app/src/test/scala/com/daml/network/integration/tests/runbook/RunbookSvPreflightIntegrationTestBase.scala b/apps/app/src/test/scala/com/daml/network/integration/tests/runbook/RunbookSvPreflightIntegrationTestBase.scala index a0124324..9b77ffb8 100644 --- a/apps/app/src/test/scala/com/daml/network/integration/tests/runbook/RunbookSvPreflightIntegrationTestBase.scala +++ b/apps/app/src/test/scala/com/daml/network/integration/tests/runbook/RunbookSvPreflightIntegrationTestBase.scala @@ -108,7 +108,8 @@ abstract class RunbookSvPreflightIntegrationTestBase } } - "The SV rewards are claimed by the SV, with 33.33% going to validator1" in { implicit env => + // TODO (#15162): re-enable after base version is 0.2.5 + "The SV rewards are claimed by the SV, with 33.33% going to validator1" ignore { implicit env => val svClient = sv_client("sv") val sv1ScanClient = scancl("sv1Scan") diff --git a/apps/common/frontend-test-utils/src/configDiffs.ts b/apps/common/frontend-test-utils/src/configDiffs.ts new file mode 100644 index 00000000..f65fa698 --- /dev/null +++ b/apps/common/frontend-test-utils/src/configDiffs.ts @@ -0,0 +1,30 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { screen } from '@testing-library/react'; +import { expect } from 'vitest'; + +export function checkAmuletRulesExpectedConfigDiffsHTML( + mockHtmlContent: string, + expectedNumberOfInFlightDiffs: number = 0 // useful when we unfold the diffs +): void { + const htmlContents = screen.getAllByTestId('config-diffs-display'); + if (expectedNumberOfInFlightDiffs > 0) { + expect(screen.getAllByTestId('folded-accordion')).toHaveLength(expectedNumberOfInFlightDiffs); + } else { + expect(screen.queryByTestId('folded-accordion')).toBeNull(); + } + expect(htmlContents[0].innerHTML).toBe(mockHtmlContent); +} + +export function checkDsoRulesExpectedConfigDiffsHTML( + mockHtmlContent: string, + expectedNumberOfInFlightDiffs: number = 0 +): void { + const htmlContents = screen.getAllByTestId('config-diffs-display'); + if (expectedNumberOfInFlightDiffs > 0) { + expect(screen.getAllByTestId('folded-accordion')).toHaveLength(expectedNumberOfInFlightDiffs); + } else { + expect(screen.queryByTestId('folded-accordion')).toBeNull(); + } + expect(htmlContents[0].innerHTML).toBe(mockHtmlContent); +} diff --git a/apps/common/frontend-test-utils/src/index.ts b/apps/common/frontend-test-utils/src/index.ts index 0dafc271..a128f1bd 100644 --- a/apps/common/frontend-test-utils/src/index.ts +++ b/apps/common/frontend-test-utils/src/index.ts @@ -1,2 +1,6 @@ export { validatorLicensesHandler } from './mocks/handlers/validator-licenses-handler'; -export { dsoInfoHandler, dsoInfo } from './mocks/handlers/dso-info-handler'; +export { dsoInfoHandler, dsoInfo, getAmuletConfig } from './mocks/handlers/dso-info-handler'; +export { + checkAmuletRulesExpectedConfigDiffsHTML, + checkDsoRulesExpectedConfigDiffsHTML, +} from './configDiffs'; diff --git a/apps/common/frontend-test-utils/src/mocks/handlers/dso-info-handler.ts b/apps/common/frontend-test-utils/src/mocks/handlers/dso-info-handler.ts index 46eeb011..cd80388e 100644 --- a/apps/common/frontend-test-utils/src/mocks/handlers/dso-info-handler.ts +++ b/apps/common/frontend-test-utils/src/mocks/handlers/dso-info-handler.ts @@ -96,10 +96,6 @@ export const dsoInfo = { _1: '2024-03-15T08:35:00Z', _2: getAmuletConfig('4815162342'), }, - { - _1: '2524-03-19T08:35:00Z', - _2: getAmuletConfig('0.03'), - }, ], }, isDevNet: true, @@ -170,6 +166,7 @@ export const dsoInfo = { state: 'DS_Operational', cometBftGenesisJson: 'TODO(#4900): share CometBFT genesis.json of founding SV node via DsoRules config.', + acsCommitmentReconciliationInterval: '1600', }, ], ], @@ -401,112 +398,140 @@ export const dsoInfo = { sv_node_states: [], // TODO(tech-debt): add better mock data }; -function getAmuletConfig(createFee: string) { +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function getAmuletConfig(createFee: string) { return { packageConfig: { - amulet: '0.1.0', - walletPayments: '0.1.0', - dsoGovernance: '0.1.0', - validatorLifecycle: '0.1.0', - amuletNameService: '0.1.0', - wallet: '0.1.0', + amuletNameService: '0.1.5', + walletPayments: '0.1.5', + dsoGovernance: '0.1.8', + validatorLifecycle: '0.1.1', + amulet: '0.1.5', + wallet: '0.1.5', }, - tickDuration: { microseconds: '150000000' }, - transferConfig: { - holdingFee: { rate: '0.0000048225' }, - extraFeaturedAppRewardAmount: '1.0', - maxNumInputs: '100', - lockHolderFee: { fee: '0.005' }, - createFee: { fee: createFee }, - maxNumLockHolders: '50', - transferFee: { - initialRate: '0.01', - steps: [ - { _1: '100.0', _2: '0.001' }, - { _1: '1000.0', _2: '0.0001' }, - { _1: '1000000.0', _2: '0.00001' }, - ], - }, - maxNumOutputs: '100', + tickDuration: { + microseconds: '600000000', }, decentralizedSynchronizer: { requiredSynchronizers: { map: [ [ - 'global-synchronizer::1220d12352e0839d9aac0a1c0c05b0eaaeb44f0aa19958cca2db37ae22c7817949a7', + 'global-domain::12200c1f141acd0b2e48defae40aa2eb3daae48e4c16b7e1fa5d9211d352cc150c81', {}, ], ], }, activeSynchronizer: - 'global-synchronizer::1220d12352e0839d9aac0a1c0c05b0eaaeb44f0aa19958cca2db37ae22c7817949a7', + 'global-domain::12200c1f141acd0b2e48defae40aa2eb3daae48e4c16b7e1fa5d9211d352cc150c81', fees: { baseRateTrafficLimits: { - burstAmount: '2000000', - burstWindow: { microseconds: '600000000' }, + burstAmount: '400000', + burstWindow: { + microseconds: '1200000000', + }, }, - extraTrafficPrice: '1.0', + extraTrafficPrice: '16.67', readVsWriteScalingFactor: '4', - minTopupAmount: '10000000', + minTopupAmount: '200000', + }, + }, + transferConfig: { + holdingFee: { + rate: '0.0000190259', + }, + extraFeaturedAppRewardAmount: '1.0', + maxNumInputs: '100', + lockHolderFee: { + fee: '0.005', + }, + createFee: { + fee: createFee, + }, + maxNumLockHolders: '50', + transferFee: { + initialRate: '0.01', + steps: [ + { + _1: '100.0', + _2: '0.001', + }, + { + _1: '1000.0', + _2: '0.0001', + }, + { + _1: '1000000.0', + _2: '0.00001', + }, + ], }, + maxNumOutputs: '100', }, issuanceCurve: { initialValue: { - validatorRewardPercentage: '0.5', - amuletToIssuePerYear: '40000000000.0', + validatorRewardPercentage: '0.05', unfeaturedAppRewardCap: '0.6', appRewardPercentage: '0.15', featuredAppRewardCap: '100.0', + amuletToIssuePerYear: '40000000000.0', validatorRewardCap: '0.2', - optValidatorFaucetCap: null, + optValidatorFaucetCap: '2.85', }, futureValues: [ { - _1: { microseconds: '15768000000000' }, + _1: { + microseconds: '15768000000000', + }, _2: { validatorRewardPercentage: '0.12', - amuletToIssuePerYear: '20000000000.0', unfeaturedAppRewardCap: '0.6', appRewardPercentage: '0.4', featuredAppRewardCap: '100.0', + amuletToIssuePerYear: '20000000000.0', validatorRewardCap: '0.2', - optValidatorFaucetCap: null, + optValidatorFaucetCap: '2.85', }, }, { - _1: { microseconds: '47304000000000' }, + _1: { + microseconds: '47304000000000', + }, _2: { validatorRewardPercentage: '0.18', - amuletToIssuePerYear: '10000000000.0', unfeaturedAppRewardCap: '0.6', appRewardPercentage: '0.62', featuredAppRewardCap: '100.0', + amuletToIssuePerYear: '10000000000.0', validatorRewardCap: '0.2', - optValidatorFaucetCap: null, + optValidatorFaucetCap: '2.85', }, }, { - _1: { microseconds: '157680000000000' }, + _1: { + microseconds: '157680000000000', + }, _2: { validatorRewardPercentage: '0.21', - amuletToIssuePerYear: '5000000000.0', unfeaturedAppRewardCap: '0.6', appRewardPercentage: '0.69', featuredAppRewardCap: '100.0', + amuletToIssuePerYear: '5000000000.0', validatorRewardCap: '0.2', - optValidatorFaucetCap: null, + optValidatorFaucetCap: '2.85', }, }, { - _1: { microseconds: '315360000000000' }, + _1: { + microseconds: '315360000000000', + }, _2: { validatorRewardPercentage: '0.2', - amuletToIssuePerYear: '2500000000.0', unfeaturedAppRewardCap: '0.6', appRewardPercentage: '0.75', featuredAppRewardCap: '100.0', + amuletToIssuePerYear: '2500000000.0', validatorRewardCap: '0.2', - optValidatorFaucetCap: null, + optValidatorFaucetCap: '2.85', }, }, ], diff --git a/apps/common/frontend/package.json b/apps/common/frontend/package.json index a542b50f..c5ac34cf 100644 --- a/apps/common/frontend/package.json +++ b/apps/common/frontend/package.json @@ -23,8 +23,11 @@ "date-fns": "2.29.3", "decimal.js-light": "2.5.1", "splitwell-openapi": "0.0.1", + "dompurify": "3.1.6", "grpc-web": "1.3.1", + "html-react-parser": "5.1.15", "jose": "4.10.3", + "jsondiffpatch": "0.6.0", "react-oidc-context": "2.2.2", "react-router-dom": "6.8.1", "sv-openapi": "0.0.1", @@ -36,13 +39,15 @@ "@tanstack/react-query-devtools": "4.29.6", "@trivago/prettier-plugin-sort-imports": "4.0.0", "@types/node": "18.11.0", + "@types/dompurify": "3.0.5", "@types/react": "18.0.28", "@types/react-dom": "18.0.11", "eslint": "8.34.0", "eslint-config-react-app": "7.0.1", "prettier": "2.8.4", "typescript": "4.9.5", - "vitest": "^2.0.5" + "vitest": "^2.0.5", + "common-test-utils": "^0.1.0" }, "exports": { ".": "./lib/index.js", diff --git a/apps/common/frontend/src/__tests__/mocks/constants.ts b/apps/common/frontend/src/__tests__/mocks/constants.ts index afe99c17..c1f56ae8 100644 --- a/apps/common/frontend/src/__tests__/mocks/constants.ts +++ b/apps/common/frontend/src/__tests__/mocks/constants.ts @@ -288,45 +288,45 @@ export const plannedVoteResult: DsoRules_CloseVoteRequestResult = { _2: { transferConfig: { createFee: { - fee: '0.0300000000', + fee: '0.03', }, holdingFee: { rate: '0.0000190259', }, transferFee: { - initialRate: '0.0100000000', + initialRate: '0.01', steps: [ { - _1: '100.0000000000', - _2: '0.0010000000', + _1: '100.0', + _2: '0.001', }, { - _1: '1000.0000000000', - _2: '0.0001000000', + _1: '1000.0', + _2: '0.0001', }, { - _1: '1000000.0000000000', - _2: '0.0000100000', + _1: '1000000.0', + _2: '0.00001', }, ], }, lockHolderFee: { - fee: '0.0060000000', + fee: '0.006', }, - extraFeaturedAppRewardAmount: '1.0000000000', + extraFeaturedAppRewardAmount: '1.0', maxNumInputs: '100', maxNumOutputs: '100', maxNumLockHolders: '50', }, issuanceCurve: { initialValue: { - amuletToIssuePerYear: '40000000000.0000000000', - validatorRewardPercentage: '0.0500000000', - appRewardPercentage: '0.1500000000', - validatorRewardCap: '0.2000000000', - featuredAppRewardCap: '100.0000000000', - unfeaturedAppRewardCap: '0.6000000000', - optValidatorFaucetCap: '2.8500000000', + amuletToIssuePerYear: '40000000000.0', + validatorRewardPercentage: '0.05', + appRewardPercentage: '0.15', + validatorRewardCap: '0.2', + featuredAppRewardCap: '100.0', + unfeaturedAppRewardCap: '0.6', + optValidatorFaucetCap: '2.85', }, futureValues: [ { @@ -334,13 +334,13 @@ export const plannedVoteResult: DsoRules_CloseVoteRequestResult = { microseconds: '15768000000000', }, _2: { - amuletToIssuePerYear: '20000000000.0000000000', - validatorRewardPercentage: '0.1200000000', - appRewardPercentage: '0.4000000000', - validatorRewardCap: '0.2000000000', - featuredAppRewardCap: '100.0000000000', - unfeaturedAppRewardCap: '0.6000000000', - optValidatorFaucetCap: '2.8500000000', + amuletToIssuePerYear: '20000000000.0', + validatorRewardPercentage: '0.12', + appRewardPercentage: '0.4', + validatorRewardCap: '0.2', + featuredAppRewardCap: '100.0', + unfeaturedAppRewardCap: '0.6', + optValidatorFaucetCap: '2.85', }, }, { @@ -348,13 +348,13 @@ export const plannedVoteResult: DsoRules_CloseVoteRequestResult = { microseconds: '47304000000000', }, _2: { - amuletToIssuePerYear: '10000000000.0000000000', - validatorRewardPercentage: '0.1800000000', - appRewardPercentage: '0.6200000000', - validatorRewardCap: '0.2000000000', - featuredAppRewardCap: '100.0000000000', - unfeaturedAppRewardCap: '0.6000000000', - optValidatorFaucetCap: '2.8500000000', + amuletToIssuePerYear: '10000000000.0', + validatorRewardPercentage: '0.18', + appRewardPercentage: '0.62', + validatorRewardCap: '0.2', + featuredAppRewardCap: '100.0', + unfeaturedAppRewardCap: '0.6', + optValidatorFaucetCap: '2.85', }, }, { @@ -362,13 +362,13 @@ export const plannedVoteResult: DsoRules_CloseVoteRequestResult = { microseconds: '157680000000000', }, _2: { - amuletToIssuePerYear: '5000000000.0000000000', - validatorRewardPercentage: '0.2100000000', - appRewardPercentage: '0.6900000000', - validatorRewardCap: '0.2000000000', - featuredAppRewardCap: '100.0000000000', - unfeaturedAppRewardCap: '0.6000000000', - optValidatorFaucetCap: '2.8500000000', + amuletToIssuePerYear: '5000000000.0', + validatorRewardPercentage: '0.21', + appRewardPercentage: '0.69', + validatorRewardCap: '0.2', + featuredAppRewardCap: '100.0', + unfeaturedAppRewardCap: '0.6', + optValidatorFaucetCap: '2.85', }, }, { @@ -376,13 +376,13 @@ export const plannedVoteResult: DsoRules_CloseVoteRequestResult = { microseconds: '315360000000000', }, _2: { - amuletToIssuePerYear: '2500000000.0000000000', - validatorRewardPercentage: '0.2000000000', - appRewardPercentage: '0.7500000000', - validatorRewardCap: '0.2000000000', - featuredAppRewardCap: '100.0000000000', - unfeaturedAppRewardCap: '0.6000000000', - optValidatorFaucetCap: '2.8500000000', + amuletToIssuePerYear: '2500000000.0', + validatorRewardPercentage: '0.2', + appRewardPercentage: '0.75', + validatorRewardCap: '0.2', + featuredAppRewardCap: '100.0', + unfeaturedAppRewardCap: '0.6', + optValidatorFaucetCap: '2.85', }, }, ], @@ -405,7 +405,7 @@ export const plannedVoteResult: DsoRules_CloseVoteRequestResult = { microseconds: '1200000000', }, }, - extraTrafficPrice: '16.6700000000', + extraTrafficPrice: '16.67', readVsWriteScalingFactor: '4', minTopupAmount: '200000', }, @@ -612,3 +612,130 @@ export const unvotedRequest = { 'CgMyLjESuQYKRQC7CjH8IFTbA2XHBj25PMQ8/J/Xn3C8eNr/ZWqb1vk3D8oQEiCtmA06+NVO1cqJJ+4KLqplpAM+Y8mmRvyg8FRGZ2dIyxIVc3BsaWNlLWRzby1nb3Zlcm5hbmNlGmEKQDE3OTBhMTE0ZjgzZDVmMjkwMjYxZmFlMWU3ZTQ2ZmJhNzVhODYxYTNkZDYwM2M2YjRlZjZiNjdiNDkwNTM5NDgSBlNwbGljZRIIRHNvUnVsZXMaC1ZvdGVSZXF1ZXN0IvUDavIDCk0KSzpJRFNPOjoxMjIwNThmZDYxYTU2NGYwMzgyM2JjMWUyN2YxNmRlNTE5Y2FiY2QzNjgxZmMzMWFiNTIzZjRlZjY5OGRjNmI2YzNhYgoTChFCD0RpZ2l0YWwtQXNzZXQtMgqmAQqjAXKgAQoMQVJDX0Rzb1J1bGVzEo8BaowBCokBCoYBcoMBChpTUkFSQ19VcGRhdGVTdlJld2FyZFdlaWdodBJlamMKWQpXOlVkaWdpdGFsLWFzc2V0LTI6OjEyMjBhMTUwNGQ4NzQ1MWNlNmE0YTEzNTVkNzFjNGYyNjY3N2Q2M2RiNWU2Mjk4ZDA0YjBkMTg2NWZkOTcxNDMyYTQxCgYKBBjAzyQKFgoUahIKBAoCQgAKCgoIQgZUZXN0IDMKCwoJKUTLwa3nIQYACrcBCrQBYrEBCq4BChFCD0RpZ2l0YWwtQXNzZXQtMhKYAWqVAQpZClc6VWRpZ2l0YWwtYXNzZXQtMjo6MTIyMGExNTA0ZDg3NDUxY2U2YTRhMTM1NWQ3MWM0ZjI2Njc3ZDYzZGI1ZTYyOThkMDRiMGQxODY1ZmQ5NzE0MzJhNDEKBAoCEAEKMgowai4KBAoCQgAKJgokQiJJIGFjY2VwdCwgYXMgSSByZXF1ZXN0ZWQgdGhlIHZvdGUuCgQKAlIAKklEU086OjEyMjA1OGZkNjFhNTY0ZjAzODIzYmMxZTI3ZjE2ZGU1MTljYWJjZDM2ODFmYzMxYWI1MjNmNGVmNjk4ZGM2YjZjM2FiOTSmQN1aIQYAQioKJgokCAESIHFq7Tf1rU33ywG032I92KCzsnvbFh0BH8vZPb2kb3FfEB4=', createdAt: '2024-09-05T08:13:23.038772Z', }; + +export function getExpectedAmuletRulesConfigDiffsHTML( + originalLockHolderFee: string, + replacementLockHolderFee: string +): string { + return ( + '
  • decentralizedSynchronizer
    {\n' +
    +    '  "requiredSynchronizers": {\n' +
    +    '    "map": [\n' +
    +    '      [\n' +
    +    '        "global-domain::122058fd61a564f03823bc1e27f16de519cabcd3681fc31ab523f4ef698dc6b6c3ab",\n' +
    +    '        {}\n' +
    +    '      ]\n' +
    +    '    ]\n' +
    +    '  },\n' +
    +    '  "activeSynchronizer": "global-domain::122058fd61a564f03823bc1e27f16de519cabcd3681fc31ab523f4ef698dc6b6c3ab",\n' +
    +    '  "fees": {\n' +
    +    '    "baseRateTrafficLimits": {\n' +
    +    '      "burstAmount": "400000",\n' +
    +    '      "burstWindow": {\n' +
    +    '        "microseconds": "1200000000"\n' +
    +    '      }\n' +
    +    '    },\n' +
    +    '    "extraTrafficPrice": "16.67",\n' +
    +    '    "readVsWriteScalingFactor": "4",\n' +
    +    '    "minTopupAmount": "200000"\n' +
    +    '  }\n' +
    +    '}
  • issuanceCurve
    {\n' +
    +    '  "initialValue": {\n' +
    +    '    "amuletToIssuePerYear": "40000000000.0",\n' +
    +    '    "validatorRewardPercentage": "0.05",\n' +
    +    '    "appRewardPercentage": "0.15",\n' +
    +    '    "validatorRewardCap": "0.2",\n' +
    +    '    "featuredAppRewardCap": "100.0",\n' +
    +    '    "unfeaturedAppRewardCap": "0.6",\n' +
    +    '    "optValidatorFaucetCap": "2.85"\n' +
    +    '  },\n' +
    +    '  "futureValues": [\n' +
    +    '    {\n' +
    +    '      "_1": {\n' +
    +    '        "microseconds": "15768000000000"\n' +
    +    '      },\n' +
    +    '      "_2": {\n' +
    +    '        "amuletToIssuePerYear": "20000000000.0",\n' +
    +    '        "validatorRewardPercentage": "0.12",\n' +
    +    '        "appRewardPercentage": "0.4",\n' +
    +    '        "validatorRewardCap": "0.2",\n' +
    +    '        "featuredAppRewardCap": "100.0",\n' +
    +    '        "unfeaturedAppRewardCap": "0.6",\n' +
    +    '        "optValidatorFaucetCap": "2.85"\n' +
    +    '      }\n' +
    +    '    },\n' +
    +    '    {\n' +
    +    '      "_1": {\n' +
    +    '        "microseconds": "47304000000000"\n' +
    +    '      },\n' +
    +    '      "_2": {\n' +
    +    '        "amuletToIssuePerYear": "10000000000.0",\n' +
    +    '        "validatorRewardPercentage": "0.18",\n' +
    +    '        "appRewardPercentage": "0.62",\n' +
    +    '        "validatorRewardCap": "0.2",\n' +
    +    '        "featuredAppRewardCap": "100.0",\n' +
    +    '        "unfeaturedAppRewardCap": "0.6",\n' +
    +    '        "optValidatorFaucetCap": "2.85"\n' +
    +    '      }\n' +
    +    '    },\n' +
    +    '    {\n' +
    +    '      "_1": {\n' +
    +    '        "microseconds": "157680000000000"\n' +
    +    '      },\n' +
    +    '      "_2": {\n' +
    +    '        "amuletToIssuePerYear": "5000000000.0",\n' +
    +    '        "validatorRewardPercentage": "0.21",\n' +
    +    '        "appRewardPercentage": "0.69",\n' +
    +    '        "validatorRewardCap": "0.2",\n' +
    +    '        "featuredAppRewardCap": "100.0",\n' +
    +    '        "unfeaturedAppRewardCap": "0.6",\n' +
    +    '        "optValidatorFaucetCap": "2.85"\n' +
    +    '      }\n' +
    +    '    },\n' +
    +    '    {\n' +
    +    '      "_1": {\n' +
    +    '        "microseconds": "315360000000000"\n' +
    +    '      },\n' +
    +    '      "_2": {\n' +
    +    '        "amuletToIssuePerYear": "2500000000.0",\n' +
    +    '        "validatorRewardPercentage": "0.2",\n' +
    +    '        "appRewardPercentage": "0.75",\n' +
    +    '        "validatorRewardCap": "0.2",\n' +
    +    '        "featuredAppRewardCap": "100.0",\n' +
    +    '        "unfeaturedAppRewardCap": "0.6",\n' +
    +    '        "optValidatorFaucetCap": "2.85"\n' +
    +    '      }\n' +
    +    '    }\n' +
    +    '  ]\n' +
    +    '}
  • packageConfig
    {\n' +
    +    '  "amulet": "0.1.5",\n' +
    +    '  "amuletNameService": "0.1.5",\n' +
    +    '  "dsoGovernance": "0.1.8",\n' +
    +    '  "validatorLifecycle": "0.1.1",\n' +
    +    '  "wallet": "0.1.5",\n' +
    +    '  "walletPayments": "0.1.5"\n' +
    +    '}
  • tickDuration
    {\n' +
    +    '  "microseconds": "600000000"\n' +
    +    '}
  • transferConfig
    • createFee
      {\n' +
      +    '  "fee": "0.03"\n' +
      +    '}
    • extraFeaturedAppRewardAmount
      "1.0"
    • holdingFee
      {\n' +
      +    '  "rate": "0.0000190259"\n' +
      +    `}
    • lockHolderFee
      • fee
        "${originalLockHolderFee}"
        "${replacementLockHolderFee}"
    • maxNumInputs
      "100"
    • maxNumLockHolders
      "50"
    • maxNumOutputs
      "100"
    • transferFee
      {\n` +
      +    '  "initialRate": "0.01",\n' +
      +    '  "steps": [\n' +
      +    '    {\n' +
      +    '      "_1": "100.0",\n' +
      +    '      "_2": "0.001"\n' +
      +    '    },\n' +
      +    '    {\n' +
      +    '      "_1": "1000.0",\n' +
      +    '      "_2": "0.0001"\n' +
      +    '    },\n' +
      +    '    {\n' +
      +    '      "_1": "1000000.0",\n' +
      +    '      "_2": "0.00001"\n' +
      +    '    }\n' +
      +    '  ]\n' +
      +    '}
    • '.trim() + ); +} diff --git a/apps/common/frontend/src/__tests__/votes/votes.test.tsx b/apps/common/frontend/src/__tests__/votes/votes.test.tsx index 23468408..4a511798 100644 --- a/apps/common/frontend/src/__tests__/votes/votes.test.tsx +++ b/apps/common/frontend/src/__tests__/votes/votes.test.tsx @@ -3,10 +3,14 @@ import { QueryClient, UseQueryResult, useQuery, QueryClientProvider } from '@tanstack/react-query'; import { render, screen, fireEvent } from '@testing-library/react'; import { DsoInfo, SvVote, VotesHooks, VotesHooksContext } from 'common-frontend'; +import { theme } from 'common-frontend'; import { Contract } from 'common-frontend-utils'; +import { checkAmuletRulesExpectedConfigDiffsHTML } from 'common-test-utils'; import React from 'react'; import { test, expect, describe } from 'vitest'; +import { ThemeProvider } from '@mui/material'; + import { VoteRequest, DsoRules_CloseVoteRequestResult, @@ -15,6 +19,7 @@ import { ContractId } from '@daml/types'; import * as constants from '../mocks/constants'; import { ListVoteRequests } from '../../components'; +import { getExpectedAmuletRulesConfigDiffsHTML } from '../mocks/constants'; const queryClient = new QueryClient(); // The linter wants me to add the constants.X in the queryKey, @@ -92,15 +97,16 @@ const provider: VotesHooks = { const TestVotes: React.FC<{ showActionNeeded: boolean }> = ({ showActionNeeded }) => { return ( - - - - - + + + + + + + ); }; -// TODO(#14439): these tests should check that the diff in the opened modal (click on the row) are correct describe('Votes list should', () => { test('Show votes requiring action, when that is enabled', async () => { render(); @@ -108,7 +114,7 @@ describe('Votes list should', () => { const actionNeeded = await screen.findByText('Action Needed'); expect(actionNeeded).toBeDefined(); fireEvent.click(actionNeeded); - + // TODO(#15151): Test diffs for SRARC_UpdateSvRewardWeight const actionNeededRows = await screen.findAllByText('SRARC_UpdateSvRewardWeight'); expect(actionNeededRows).toHaveLength(1); }); @@ -127,6 +133,12 @@ describe('Votes list should', () => { const plannedRows = await screen.findAllByText('CRARC_AddFutureAmuletConfigSchedule'); expect(plannedRows).toHaveLength(1); + + const action = plannedRows[0]; // Use the first element from the array + fireEvent.click(action); + + const mockHtmlContent = getExpectedAmuletRulesConfigDiffsHTML('0.005', '0.006'); + checkAmuletRulesExpectedConfigDiffsHTML(mockHtmlContent, 0); }); test('Show votes that are executed', async () => { diff --git a/apps/common/frontend/src/components/AccordionList.tsx b/apps/common/frontend/src/components/AccordionList.tsx new file mode 100644 index 00000000..588a0271 --- /dev/null +++ b/apps/common/frontend/src/components/AccordionList.tsx @@ -0,0 +1,88 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { ReactNode } from 'react'; + +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { Stack } from '@mui/material'; +import Accordion from '@mui/material/Accordion'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import Typography from '@mui/material/Typography'; + +interface AccordionItemProps { + title: React.ReactNode; + content: React.ReactNode; + initiallyOpen: boolean; +} + +const AccordionItem: React.FC = ({ title, content, initiallyOpen }) => { + const [expanded, setExpanded] = React.useState(initiallyOpen); + + const handleChange = (isExpanded: boolean) => { + setExpanded(isExpanded); + }; + + return ( + handleChange(isExpanded)}> + } + aria-controls={`${title}-content`} + id={`${title}-header`} + > + {title} + + {content} + + ); +}; + +export interface AccordionListProps { + unfoldedAccordions: { title: React.ReactNode; content: React.ReactNode }[]; + foldedAccordions: { title: React.ReactNode; content: React.ReactNode }[]; +} + +const AccordionList: React.FC = ({ unfoldedAccordions, foldedAccordions }) => { + return ( + + Config diffs + {unfoldedAccordions.map((accordion, index) => ( + + {accordion.title} + + } + content={accordion.content} + initiallyOpen + /> + ))} + {foldedAccordions.map((accordion, index) => ( + + {accordion.title} + + } + content={accordion.content} + initiallyOpen={false} + data-testid={'folded-accordion'} + /> + ))} + + ); +}; + +const ComponentHeaderWrapper: React.FC<{ header: string; children: ReactNode }> = ({ + header, + children, +}) => { + return ( + <> + {header}: {children} + + ); +}; + +export default AccordionList; diff --git a/apps/common/frontend/src/components/PrettyJsonDiff.tsx b/apps/common/frontend/src/components/PrettyJsonDiff.tsx new file mode 100644 index 00000000..6c0aaf67 --- /dev/null +++ b/apps/common/frontend/src/components/PrettyJsonDiff.tsx @@ -0,0 +1,214 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// @ts-ignore +import * as jsondiffpatch from 'jsondiffpatch'; +// @ts-ignore +import * as htmlFormatter from 'jsondiffpatch/formatters/html'; +import DiffMatchPatch from 'diff-match-patch'; +import DOMPurify from 'dompurify'; +import parse from 'html-react-parser'; +import React from 'react'; + +import { Box } from '@mui/material'; +import { GlobalStyles } from '@mui/system'; + +import { AmuletConfig, USD } from '../../daml.js/splice-amulet-0.1.5/lib/Splice/AmuletConfig'; +import { DsoRulesConfig } from '../../daml.js/splice-dso-governance-0.1.8/lib/Splice/DsoRules'; + +const jsondiffpatchInstance = jsondiffpatch.create({ + arrays: { + detectMove: true, + includeValueOnMove: false, + }, + textDiff: { + diffMatchPatch: DiffMatchPatch, + minLength: 60, + }, + cloneDiffValues: true, +}); + +const JsonDiffStyles = () => ( + // original template: https://esm.sh/jsondiffpatch@0.6.0/lib/formatters/styles/html.css + .jsondiffpatch-value': { + transition: 'all 0.5s', + overflowY: 'hidden', + }, + '.jsondiffpatch-unchanged-showing .jsondiffpatch-unchanged, .jsondiffpatch-unchanged-showing .jsondiffpatch-movedestination > .jsondiffpatch-value': + { + maxHeight: '100px', + }, + '.jsondiffpatch-unchanged-hidden .jsondiffpatch-unchanged, .jsondiffpatch-unchanged-hidden .jsondiffpatch-movedestination > .jsondiffpatch-value': + { + maxHeight: 0, + }, + '.jsondiffpatch-unchanged-hiding .jsondiffpatch-movedestination > .jsondiffpatch-value, .jsondiffpatch-unchanged-hidden .jsondiffpatch-movedestination > .jsondiffpatch-value': + { + display: 'block', + }, + '.jsondiffpatch-unchanged-visible .jsondiffpatch-unchanged, .jsondiffpatch-unchanged-visible .jsondiffpatch-movedestination > .jsondiffpatch-value': + { + maxHeight: '100px', + }, + '.jsondiffpatch-unchanged-hiding .jsondiffpatch-unchanged, .jsondiffpatch-unchanged-hiding .jsondiffpatch-movedestination > .jsondiffpatch-value': + { + maxHeight: 0, + }, + '.jsondiffpatch-unchanged-showing .jsondiffpatch-arrow, .jsondiffpatch-unchanged-hiding .jsondiffpatch-arrow': + { + display: 'none', + }, + '.jsondiffpatch-value': { + display: 'inline-block', + }, + '.jsondiffpatch-property-name': { + display: 'inline-block', + paddingRight: '5px', + verticalAlign: 'top', + }, + '.jsondiffpatch-property-name:after': { + content: '": "', // Correct usage of quotes + }, + '.jsondiffpatch-child-node-type-array > .jsondiffpatch-property-name:after': { + content: '": ["', // Correct usage of quotes + }, + '.jsondiffpatch-child-node-type-array:after': { + content: '"],"', // Correct usage of quotes + }, + 'div.jsondiffpatch-child-node-type-array:before': { + content: '"["', // Correct usage of quotes + }, + 'div.jsondiffpatch-child-node-type-array:after': { + content: '"]"', // Correct usage of quotes + }, + '.jsondiffpatch-child-node-type-object > .jsondiffpatch-property-name:after': { + content: '": {"', // Correct usage of quotes + }, + '.jsondiffpatch-child-node-type-object:after': { + content: '"}, "', // Correct usage of quotes + }, + 'div.jsondiffpatch-child-node-type-object:before': { + content: '"{"', // Correct usage of quotes + }, + 'div.jsondiffpatch-child-node-type-object:after': { + content: '"}"', // Correct usage of quotes + }, + '.jsondiffpatch-value pre:after': { + content: '","', // Correct usage of quotes + }, + 'li:last-child > .jsondiffpatch-value pre:after, .jsondiffpatch-modified > .jsondiffpatch-left-value pre:after': + { + content: '""', // Correct usage of quotes + }, + '.jsondiffpatch-modified .jsondiffpatch-value': { + display: 'inline-block', + }, + '.jsondiffpatch-modified .jsondiffpatch-right-value': { + marginLeft: '5px', + }, + '.jsondiffpatch-moved .jsondiffpatch-value': { + display: 'none', + }, + '.jsondiffpatch-moved .jsondiffpatch-moved-destination': { + display: 'inline-block', + background: '#ffffbb', + color: '#888', + }, + '.jsondiffpatch-moved .jsondiffpatch-moved-destination:before': { + content: '" => "', // Correct usage of quotes + }, + 'ul.jsondiffpatch-textdiff': { + padding: 0, + }, + '.jsondiffpatch-textdiff-location': { + color: '#bbb', + display: 'inline-block', + minWidth: '60px', + }, + '.jsondiffpatch-textdiff-line': { + display: 'inline-block', + }, + '.jsondiffpatch-textdiff-line-number:after': { + content: '","', // Correct usage of quotes + }, + '.jsondiffpatch-error': { + background: 'red', + color: 'white', + fontWeight: 'bold', + }, + }} + /> +); + +interface PrettyJsonDiffProps { + data?: DsoRulesConfig | AmuletConfig; + compareWithData?: DsoRulesConfig | AmuletConfig; +} + +export const PrettyJsonDiff: React.FC = ({ data, compareWithData }) => { + // Calculate the difference between data objects + const delta = jsondiffpatchInstance.diff(compareWithData, data); + + // If there's no difference, render the data as pretty-printed JSON + if (!delta) { + return ( + + {JSON.stringify(data, null, 2)} + + ); + } + + // Sanitize and format the HTML for the diff 'display' + // @ts-ignore + const sanatizedHtml = DOMPurify.sanitize(htmlFormatter.format(delta, compareWithData)); + + return ( + <> + + + {parse(sanatizedHtml)} + + + ); +}; diff --git a/apps/common/frontend/src/components/index.ts b/apps/common/frontend/src/components/index.ts index 7fa641c4..5a4201b1 100644 --- a/apps/common/frontend/src/components/index.ts +++ b/apps/common/frontend/src/components/index.ts @@ -29,9 +29,11 @@ import { VotesHooks, useVotesHooks, ListVoteRequests, + ActionView, } from './votes'; export { + ActionView, AmountDisplay, AuthProvider, AnsEntry, diff --git a/apps/common/frontend/src/components/votes/ActionView.tsx b/apps/common/frontend/src/components/votes/ActionView.tsx index c3b4ab4c..4d7be6f8 100644 --- a/apps/common/frontend/src/components/votes/ActionView.tsx +++ b/apps/common/frontend/src/components/votes/ActionView.tsx @@ -1,6 +1,16 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { DateDisplay, Loading, PartyId, useVotesHooks } from 'common-frontend'; +import { QueryObserverSuccessResult } from '@tanstack/react-query'; +import { + BaseVotesHooks, + DateDisplay, + getAmuletConfigurationAsOfNow, + Loading, + PartyId, + useVotesHooks, + VotesHooks, +} from 'common-frontend'; +import dayjs from 'dayjs'; import React from 'react'; import { @@ -14,15 +24,188 @@ import { } from '@mui/material'; import { AmuletConfig, USD } from '@daml.js/splice-amulet/lib/Splice/AmuletConfig'; +import { + AmuletRules_AddFutureAmuletConfigSchedule, + AmuletRules_UpdateFutureAmuletConfigSchedule, + AmuletRules_RemoveFutureAmuletConfigSchedule, +} from '@daml.js/splice-amulet/lib/Splice/AmuletRules'; +import { Schedule } from '@daml.js/splice-amulet/lib/Splice/Schedule'; import { ActionRequiringConfirmation, + DsoRules_CloseVoteRequestResult, + DsoRules_SetConfig, DsoRulesConfig, + VoteRequest, } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules/module'; +import { Time } from '@daml/types'; + +import AccordionList, { AccordionListProps } from '../AccordionList'; +import { DsoInfo } from '../Dso'; +import { PrettyJsonDiff } from '../PrettyJsonDiff'; +import { getAction } from './ListVoteRequests'; +import { VoteRequestResultTableType } from './VoteResultsFilterTable'; + +import ARC_DsoRules = ActionRequiringConfirmation.ARC_DsoRules; +import ARC_AmuletRules = ActionRequiringConfirmation.ARC_AmuletRules; + +/* + * This function finds the latest vote result for a given action name and time. + * It is used to compare the current vote result with the one that was before (current -1). + */ +function findLatestVoteResult( + time: string, + actionName: string, + votesHooks: VotesHooks, + tableType?: VoteRequestResultTableType +): DsoRules_CloseVoteRequestResult | undefined { + const voteResultsQuery = votesHooks.useListVoteRequestResult( + 1, + actionName, + undefined, + undefined, + time, + tableType !== 'Rejected' + ); + if (!voteResultsQuery.data || !voteResultsQuery.data[0]) { + return undefined; + } else { + return DsoRules_CloseVoteRequestResult.encode( + voteResultsQuery.data[0] + ) as DsoRules_CloseVoteRequestResult; + } +} + +/* + * This function filters out the in-flight vote requests of a given action name. + */ +function getInflightVoteRequests( + actionName: string, + voteRequests: VoteRequest[] | undefined +): VoteRequest[] | [] { + if (!voteRequests) { + return []; + } + return voteRequests.filter(voteRequest => { + const tag = getAction(voteRequest.action); + return tag === actionName; + }); +} + +/* + * This function finds the AmuletRules schedule item that it will replace if voted directly. + * It is used for CRARC_AddFutureAmuletConfigSchedule and CRARC_UpdateFutureAmuletConfigSchedule actions. + * It compares the current schedule with: + * - the current AmuletRules contract to diff against the schedule it will replace (nth -1) [always] + * - the latest vote result to diff against [when in Planned, Executed or Rejected tabs] + * - the in-flight vote requests to diff against [when in Action Needed or In Progress tabs] + */ +function findAmuletRulesScheduleItemToCompareAgainst( + schedule: Schedule>, + scheduleTime: string, + votesHooks: VotesHooks, + defaultConfig: AmuletConfig, + tableType?: VoteRequestResultTableType +): [string, AmuletConfig] { + function parseAmuletRulesAction( + action: ActionRequiringConfirmation + ): [string, AmuletConfig] { + if (action.tag === 'ARC_AmuletRules') { + const amuletRulesAction = action.value.amuletRulesAction; + switch (amuletRulesAction.tag) { + case 'CRARC_AddFutureAmuletConfigSchedule': + return [ + amuletRulesAction.value.newScheduleItem._1, + amuletRulesAction.value.newScheduleItem._2, + ]; + case 'CRARC_UpdateFutureAmuletConfigSchedule': + return [amuletRulesAction.value.scheduleItem._1, amuletRulesAction.value.scheduleItem._2]; + } + } + return ['initial', defaultConfig]; + } + const latestAddAction = findLatestVoteResult( + scheduleTime, + 'CRARC_AddFutureAmuletConfigSchedule', + votesHooks, + tableType + )?.request.action; + const latestUpdateAction = findLatestVoteResult( + scheduleTime, + 'CRARC_UpdateFutureAmuletConfigSchedule', + votesHooks, + tableType + )?.request.action; + + const currentAmuletConfig = Schedule(Time, AmuletConfig(USD)).encode( + getAmuletConfigurationAsOfNow(schedule) + ) as Schedule>; + + const isExecutedOrRejected = tableType === 'Executed' || tableType === 'Rejected'; + + if (isExecutedOrRejected) { + if (!latestAddAction) { + if (!latestUpdateAction) { + //TODO(#14813): Store a copy of the initial DsoRules and AmuletRules to diff against initial configs + return ['initial', defaultConfig]; + } else { + return parseAmuletRulesAction(latestUpdateAction); + } + } else { + if (!latestUpdateAction) { + return parseAmuletRulesAction(latestAddAction); + } else { + const latestAdd = parseAmuletRulesAction(latestAddAction); + const latestUpdate = parseAmuletRulesAction(latestUpdateAction); + if (dayjs(latestAdd[0]).isAfter(dayjs(latestUpdate[0]))) { + return latestAdd; + } else { + return latestUpdate; + } + } + } + } + // for planned and executed sections, the values are already part of the configSchedule + // for the action needed and in progress sections, the values are not part of the configSchedule + if (currentAmuletConfig.futureValues.length <= 1) { + return ['initial', currentAmuletConfig.initialValue]; + } + + const scheduleTimeDayjs = dayjs(scheduleTime); + + let currentConfigIndex = currentAmuletConfig.futureValues.findIndex(e => + dayjs(e._1).isSame(scheduleTimeDayjs) + ); + + if (currentConfigIndex === -1) { + currentConfigIndex = currentAmuletConfig.futureValues.findIndex( + e => !dayjs(e._1).isBefore(scheduleTimeDayjs) + ); + } + + if (currentConfigIndex === -1) { + const config = currentAmuletConfig.futureValues[currentAmuletConfig.futureValues.length - 1]; + return [dayjs(config._1).toString(), config._2]; + } + + if (currentConfigIndex === 0) { + return ['initial', currentAmuletConfig.initialValue]; + } + const config = currentAmuletConfig.futureValues[currentConfigIndex - 1]; + return [dayjs(config._1).toString(), config._2]; +} -const ActionView: React.FC<{ action: ActionRequiringConfirmation }> = ({ action }) => { +export const ActionView: React.FC<{ + action: ActionRequiringConfirmation; + tableType?: VoteRequestResultTableType; + expiresAt?: string; +}> = ({ action, tableType, expiresAt }) => { const votesHooks = useVotesHooks(); const dsoInfosQuery = votesHooks.useDsoInfos(); + if (!action) { + return

      No action specified

      ; + } + if (dsoInfosQuery.isLoading) { return ; } @@ -75,15 +258,17 @@ const ActionView: React.FC<{ action: ActionRequiringConfirmation }> = ({ action } case 'SRARC_SetConfig': { return ( - , - }} + dsoAction={dsoAction} + expiresAt={expiresAt} + tableType={tableType} /> ); } + // TODO(#15151): implement diffs for UpdateSvRewardWeight case 'SRARC_UpdateSvRewardWeight': { return ( = ({ action switch (amuletRulesAction.tag) { case 'CRARC_AddFutureAmuletConfigSchedule': { return ( - - ), - NewScheduleItem: ( - - ), - }} + amuletRulesAction={amuletRulesAction} + tableType={tableType} /> ); } case 'CRARC_RemoveFutureAmuletConfigSchedule': { return ( - , - ScheduleItem: ( - e._1 === amuletRulesAction.value.scheduleTime - )?._2 - } - /> - ), - }} + amuletRulesAction={amuletRulesAction} + tableType={tableType} /> ); } case 'CRARC_UpdateFutureAmuletConfigSchedule': { return ( - - ), - ScheduleItem: , - }} + amuletRulesAction={amuletRulesAction} + tableType={tableType} /> ); } @@ -165,7 +333,8 @@ const ActionValueTable: React.FC<{ actionType: string; actionName: string; valuesMap?: { [key: string]: React.ReactElement }; -}> = ({ actionType, actionName, valuesMap }) => { + accordionList?: AccordionListProps; +}> = ({ actionType, actionName, valuesMap, accordionList }) => { return ( <> @@ -203,20 +372,321 @@ const ActionValueTable: React.FC<{ + {accordionList && ( + + )} ); }; -const PrettyJsonPrint: React.FC<{ - data?: DsoRulesConfig | AmuletConfig | string; -}> = ({ data }) => { +const AddFutureConfigValueTable: React.FC<{ + votesHooks: BaseVotesHooks; + dsoInfosQuery: QueryObserverSuccessResult; + actionType: string; + amuletRulesAction: { + tag: 'CRARC_AddFutureAmuletConfigSchedule'; + value: AmuletRules_AddFutureAmuletConfigSchedule; + }; + tableType?: VoteRequestResultTableType; +}> = ({ votesHooks, dsoInfosQuery, actionType, amuletRulesAction, tableType }) => { + const voteRequests = votesHooks.useListDsoRulesVoteRequests(); + + if (voteRequests.isLoading) { + return ; + } + + if (voteRequests.isError) { + return

      Error, something went wrong.

      ; + } + + if (!voteRequests.data) { + return

      no VoteRequest contractId is specified

      ; + } + + const amuletConfigToCompareWith = findAmuletRulesScheduleItemToCompareAgainst( + dsoInfosQuery.data?.amuletRules.payload.configSchedule, + amuletRulesAction.value.newScheduleItem._1, + votesHooks, + amuletRulesAction.value.newScheduleItem._2, + tableType + ); + + const inflightVoteRequests: [string, AmuletConfig][] = !tableType + ? getInflightVoteRequests( + amuletRulesAction.tag, + voteRequests.data.map(vr => vr.payload) + ) + .map(vr => { + const newConfig = (vr.action.value as ARC_AmuletRules).amuletRulesAction + ?.value as AmuletRules_AddFutureAmuletConfigSchedule; + return [ + newConfig.newScheduleItem._1, + AmuletConfig(USD).encode(newConfig.newScheduleItem._2), + ] as [string, AmuletConfig]; + }) + .filter(v => v[0] !== amuletRulesAction.value.newScheduleItem._1) + : []; + + return ( + + ), + }} + accordionList={{ + unfoldedAccordions: [ + { + title: , + content: ( + + ), + }, + ], + foldedAccordions: inflightVoteRequests.map(vr => ({ + title: , + content: ( + + ), + })), + }} + /> + ); +}; + +const RemoveFutureConfigValueTable: React.FC<{ + votesHooks: BaseVotesHooks; + dsoInfosQuery: QueryObserverSuccessResult; + actionType: string; + amuletRulesAction: { + tag: 'CRARC_RemoveFutureAmuletConfigSchedule'; + value: AmuletRules_RemoveFutureAmuletConfigSchedule; + }; + tableType?: VoteRequestResultTableType; +}> = ({ votesHooks, dsoInfosQuery, actionType, amuletRulesAction, tableType }) => { + const voteRequests = votesHooks.useListDsoRulesVoteRequests(); + + if (voteRequests.isLoading) { + return ; + } + + if (voteRequests.isError) { + return

      Error, something went wrong.

      ; + } + + if (!voteRequests.data) { + return

      no VoteRequest contractId is specified

      ; + } + const amuletConfigToCompareWith = findAmuletRulesScheduleItemToCompareAgainst( + dsoInfosQuery.data?.amuletRules.payload.configSchedule, + amuletRulesAction.value.scheduleTime, + votesHooks, + dsoInfosQuery.data?.amuletRules.payload.configSchedule.initialValue, + tableType + ); + // TODO(#15154): Implement config diffs of CRARC_RemoveFutureAmuletConfigSchedule action + return ( + , + 'Comparing against config from': ( + + ), + ScheduleItem: ( + + ), + }} + /> + ); +}; + +const UpdateFutureConfigValueTable: React.FC<{ + votesHooks: BaseVotesHooks; + dsoInfosQuery: QueryObserverSuccessResult; + actionType: string; + amuletRulesAction: { + tag: 'CRARC_UpdateFutureAmuletConfigSchedule'; + value: AmuletRules_UpdateFutureAmuletConfigSchedule; + }; + tableType?: VoteRequestResultTableType; +}> = ({ votesHooks, dsoInfosQuery, actionType, amuletRulesAction, tableType }) => { + const voteRequests = votesHooks.useListDsoRulesVoteRequests(); + + if (voteRequests.isLoading) { + return ; + } + + if (voteRequests.isError) { + return

      Error, something went wrong.

      ; + } + + if (!voteRequests.data) { + return

      no VoteRequest contractId is specified

      ; + } + + const amuletConfigToCompareWith = findAmuletRulesScheduleItemToCompareAgainst( + dsoInfosQuery.data?.amuletRules.payload.configSchedule, + amuletRulesAction.value.scheduleItem._1, + votesHooks, + amuletRulesAction.value.scheduleItem._2, + tableType + ); + + const inflightVoteRequests: [string, AmuletConfig][] = !tableType + ? getInflightVoteRequests( + amuletRulesAction.tag, + voteRequests.data.map(vr => vr.payload) + ) + .map(vr => { + const newConfig = (vr.action.value as ARC_AmuletRules).amuletRulesAction + ?.value as AmuletRules_UpdateFutureAmuletConfigSchedule; + return [ + newConfig.scheduleItem._1, + AmuletConfig(USD).encode(newConfig.scheduleItem._2), + ] as [string, AmuletConfig]; + }) + .filter(v => v[0] !== amuletRulesAction.value.scheduleItem._1) + : []; + + return ( + , + }} + accordionList={{ + unfoldedAccordions: [ + { + title: , + content: ( + + ), + }, + ], + foldedAccordions: inflightVoteRequests.map(vr => ({ + title: , + content: ( + + ), + })), + }} + /> + ); +}; + +const DateOrTextDisplay = (props: { datetime: string | Date | undefined }) => { + if (props.datetime && (props.datetime instanceof Date || props.datetime !== 'initial')) { + return ; + } else { + return <>initial; + } +}; + +const SetConfigValueTable: React.FC<{ + votesHooks: BaseVotesHooks; + dsoInfosQuery: QueryObserverSuccessResult; + actionType: string; + dsoAction: { tag: 'SRARC_SetConfig'; value: DsoRules_SetConfig }; + expiresAt?: string; + tableType?: VoteRequestResultTableType; +}> = ({ votesHooks, dsoInfosQuery, actionType, dsoAction, expiresAt, tableType }) => { + const voteRequests = votesHooks.useListDsoRulesVoteRequests(); + + if (voteRequests.isLoading) { + return ; + } + + if (voteRequests.isError) { + return

      Error, something went wrong.

      ; + } + + if (!voteRequests.data) { + return

      no VoteRequest contractId is specified

      ; + } + + // 1... we need to pass the schedule time or adapt findLatestVoteResult to SetConfig + const latestConfig = findLatestVoteResult( + expiresAt || dayjs().toString(), + 'SRARC_SetConfig', + votesHooks, + tableType + ); + + const dsoConfigToCompareWith: [string, DsoRulesConfig] = !latestConfig + ? [ + 'initial', + DsoRulesConfig.encode(dsoInfosQuery.data.dsoRules.payload.config) as DsoRulesConfig, + ] + : [ + latestConfig.request.voteBefore, + ((latestConfig.request.action.value as ARC_DsoRules).dsoAction.value as DsoRules_SetConfig) + .newConfig, + ]; + + const inflightVoteRequests: [string, DsoRulesConfig][] = !tableType + ? getInflightVoteRequests( + dsoAction.tag, + voteRequests.data.map(vr => vr.payload) + ) + .map(vr => { + const newConfig = (vr.action.value as ARC_DsoRules).dsoAction + ?.value as DsoRules_SetConfig; + return [vr.voteBefore, DsoRulesConfig.encode(newConfig.newConfig)] as [ + string, + DsoRulesConfig + ]; + }) + .filter(v => !dayjs(v[0]).isSame(dayjs(expiresAt))) + : []; + return ( -
      -      {typeof data !== 'string' ? JSON.stringify(data, null, 2) : data}
      -    
      + , + }} + accordionList={{ + unfoldedAccordions: [ + { + title: , + content: ( + + ), + }, + ], + foldedAccordions: inflightVoteRequests.map(vr => ({ + title: , + content: , + })), + }} + /> ); }; diff --git a/apps/common/frontend/src/components/votes/ListVoteRequests.tsx b/apps/common/frontend/src/components/votes/ListVoteRequests.tsx index 8e282d5a..4fcf3072 100644 --- a/apps/common/frontend/src/components/votes/ListVoteRequests.tsx +++ b/apps/common/frontend/src/components/votes/ListVoteRequests.tsx @@ -28,10 +28,10 @@ import { } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules'; import { ContractId } from '@daml/types'; -import { ListVoteRequestsFilterTable } from './VoteRequestFilterTable'; +import { VoteRequestsFilterTable } from './VoteRequestFilterTable'; import VoteRequestModalContent from './VoteRequestModalContent'; import { VoteResultModalContent } from './VoteResultModalContent'; -import { VoteResultsFilterTable } from './VoteResultsFilterTable'; +import { VoteRequestResultTableType, VoteResultsFilterTable } from './VoteResultsFilterTable'; dayjs.extend(utc); @@ -48,6 +48,18 @@ interface TabPanelProps { value: number; } +export function getAction(action: ActionRequiringConfirmation): string { + if (action.tag === 'ARC_DsoRules') { + const dsoRulesAction = action.value.dsoAction; + return `${dsoRulesAction.tag}`; + } else if (action.tag === 'ARC_AmuletRules') { + const amuletRulesAction = action.value.amuletRulesAction; + return `${amuletRulesAction.tag}`; + } else { + return 'Action tag not defined.'; + } +} + const TabPanel = (props: TabPanelProps) => { const { children, value, index, ...other } = props; return ( @@ -71,6 +83,19 @@ interface ListVoteRequestsProps { ) => React.ReactNode; } +export type VoteResultModalState = + | { open: false } + | { + open: true; + tableType: VoteRequestResultTableType; + voteResult: DsoRules_CloseVoteRequestResult; + effectiveAt: string; + }; + +export type VoteRequestModalState = + | { open: false } + | { open: true; voteRequestContractId: ContractId; effectiveAt: string }; + export const ListVoteRequests: React.FC = ({ showActionNeeded, voteForm, @@ -99,28 +124,24 @@ export const ListVoteRequests: React.FC = ({ const votesQuery = votesHooks.useListVotes(voteRequestIds); const dsoInfosQuery = votesHooks.useDsoInfos(); - const [voteRequestContractId, setVoteRequestContractId] = useState< - ContractId | undefined - >(undefined); - const [voteResult, setVoteResult] = useState( - undefined - ); - const [isVoteRequestModalOpen, setVoteRequestModalOpen] = useState(false); - const [isVoteResultModalOpen, setVoteResultModalOpen] = useState(false); + const [voteRequestModalState, setVoteRequestModalState] = useState({ + open: false, + }); + const [voteResultModalState, setVoteResultModalState] = useState({ + open: false, + }); - const openModalWithVoteRequest = (voteRequestContractId: ContractId) => { - setVoteRequestContractId(voteRequestContractId); - setVoteRequestModalOpen(true); + const openModalWithVoteRequest = (voteRequestModalState: VoteRequestModalState) => { + setVoteRequestModalState(voteRequestModalState); }; - const openModalWithVoteResult = (voteResult: DsoRules_CloseVoteRequestResult) => { - setVoteResult(voteResult); - setVoteResultModalOpen(true); + const openModalWithVoteResult = (voteResultModalState: VoteResultModalState) => { + setVoteResultModalState(voteResultModalState); }; const handleClose = () => { - setVoteRequestModalOpen(false); - setVoteResultModalOpen(false); + setVoteRequestModalState({ open: false }); + setVoteResultModalState({ open: false }); }; const svPartyId = dsoInfosQuery.data?.svPartyId; @@ -139,7 +160,7 @@ export const ListVoteRequests: React.FC = ({ return

      Error, something went wrong.

      ; } - const voteRequests = listVoteRequestsQuery.data.sort((a, b) => { + const voteRequests = [...listVoteRequestsQuery.data].sort((a, b) => { const createdAtA = a.createdAt; const createdAtB = b.createdAt; if (createdAtA === createdAtB) { @@ -158,37 +179,6 @@ export const ListVoteRequests: React.FC = ({ alreadyVotedRequestIds.has(v.payload.trackingCid || v.contractId) ); - function getAction(action: ActionRequiringConfirmation) { - if (action.tag === 'ARC_DsoRules') { - const dsoRulesAction = action.value.dsoAction; - switch (dsoRulesAction.tag) { - case 'SRARC_OffboardSv': { - return `${dsoRulesAction.tag}`; - } - case 'SRARC_GrantFeaturedAppRight': { - return `${dsoRulesAction.tag}`; - } - case 'SRARC_RevokeFeaturedAppRight': { - return `${dsoRulesAction.tag}`; - } - case 'SRARC_SetConfig': { - return `${dsoRulesAction.tag}`; - } - case 'SRARC_UpdateSvRewardWeight': { - return `${dsoRulesAction.tag}`; - } - } - } else if (action.tag === 'ARC_AmuletRules') { - const amuletRulesAction = action.value.amuletRulesAction; - switch (amuletRulesAction.tag) { - default: { - return `${amuletRulesAction.tag}`; - } - } - } - return 'Action tag not defined.'; - } - const tabsToTabPanel = ( showActionNeeded ? [ @@ -202,7 +192,7 @@ export const ListVoteRequests: React.FC = ({ /> ), () => ( - = ({ /> ), () => ( - = ({ ))} = ({ } /> - {voteRequestContractId && ( + {voteRequestModalState.open && ( )} @@ -335,7 +326,7 @@ export const ListVoteRequests: React.FC = ({ = ({ } /> - + diff --git a/apps/common/frontend/src/components/votes/VoteModalContent.tsx b/apps/common/frontend/src/components/votes/VoteModalContent.tsx index 26f330d5..63b2fa17 100644 --- a/apps/common/frontend/src/components/votes/VoteModalContent.tsx +++ b/apps/common/frontend/src/components/votes/VoteModalContent.tsx @@ -27,8 +27,9 @@ import { ContractId, Party } from '@daml/types'; import { Reason } from '../../models'; import ActionView from './ActionView'; +import { VoteRequestResultTableType } from './VoteResultsFilterTable'; -interface VoteRequestModalProps { +interface VoteModalProps { voteRequestContractId: ContractId; actionReq: ActionRequiringConfirmation; requester: Party; @@ -36,15 +37,16 @@ interface VoteRequestModalProps { voteBefore: Date; rejectedVotes: SvVote[]; acceptedVotes: SvVote[]; - handleClose: () => void; voteForm?: ( voteRequestContractId: ContractId, currentSvVote: SvVote | undefined ) => React.ReactNode; curSvVote?: SvVote; + tableType?: VoteRequestResultTableType; + effectiveAt?: string; } -const VoteModalContent: React.FC = ({ +const VoteModalContent: React.FC = ({ voteRequestContractId, actionReq, requester, @@ -52,9 +54,10 @@ const VoteModalContent: React.FC = ({ voteBefore, rejectedVotes, acceptedVotes, - handleClose, voteForm, curSvVote, + tableType, + effectiveAt, }) => { const votesHooks = useVotesHooks(); @@ -73,7 +76,7 @@ const VoteModalContent: React.FC = ({ Requested Action - + Request Information diff --git a/apps/common/frontend/src/components/votes/VoteRequestFilterTable.tsx b/apps/common/frontend/src/components/votes/VoteRequestFilterTable.tsx index 949501aa..ef0b34ba 100644 --- a/apps/common/frontend/src/components/votes/VoteRequestFilterTable.tsx +++ b/apps/common/frontend/src/components/votes/VoteRequestFilterTable.tsx @@ -11,16 +11,17 @@ import { ActionRequiringConfirmation, VoteRequest, } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules/module'; -import { ContractId } from '@daml/types'; + +import { VoteRequestModalState } from './ListVoteRequests'; interface ListVoteRequestsTableProps { voteRequests: Contract[]; getAction: (action: ActionRequiringConfirmation) => string; - openModalWithVoteRequest: (voteRequestContractId: ContractId) => void; + openModalWithVoteRequest: (voteRequestModalState: VoteRequestModalState) => void; tableBodyId: string; } -export const ListVoteRequestsFilterTable: React.FC = ({ +export const VoteRequestsFilterTable: React.FC = ({ voteRequests, getAction, openModalWithVoteRequest, @@ -102,7 +103,11 @@ export const ListVoteRequestsFilterTable: React.FC = })); const handleRowClick: GridEventListener<'rowClick'> = (params: GridRowParams) => { - openModalWithVoteRequest(params.row.trackingCid); + openModalWithVoteRequest({ + open: true, + voteRequestContractId: params.row.trackingCid, + effectiveAt: params.row.expiresAt.toISOString(), + }); }; return ( diff --git a/apps/common/frontend/src/components/votes/VoteRequestModalContent.tsx b/apps/common/frontend/src/components/votes/VoteRequestModalContent.tsx index 9b6af167..6ca6f62b 100644 --- a/apps/common/frontend/src/components/votes/VoteRequestModalContent.tsx +++ b/apps/common/frontend/src/components/votes/VoteRequestModalContent.tsx @@ -17,16 +17,18 @@ interface VoteRequestModalProps { voteRequestContractId: ContractId, currentSvVote: SvVote | undefined ) => React.ReactNode; + effectiveAt?: string; } const VoteRequestModalContent: React.FC = ({ voteRequestContractId, handleClose, voteForm, + effectiveAt, }) => { const votesHooks = useVotesHooks(); const voteRequestQuery = votesHooks.useVoteRequest(voteRequestContractId); - + const voteRequests = votesHooks.useListDsoRulesVoteRequests(); const votesQuery = votesHooks.useListVotes([voteRequestContractId]); // allVotes being empty means that the vote request has been executed, as the initiator of the request must vote on his proposition. Therefore, we can close the modal. @@ -39,15 +41,15 @@ const VoteRequestModalContent: React.FC = ({ const dsoInfosQuery = votesHooks.useDsoInfos(); const svPartyId = dsoInfosQuery.data?.svPartyId; - if (voteRequestQuery.isLoading) { + if (voteRequestQuery.isLoading || voteRequests.isLoading) { return ; } - if (voteRequestQuery.isError) { + if (voteRequestQuery.isError || voteRequests.isError) { return

      Error, something went wrong.

      ; } - if (!voteRequestQuery.data) { + if (!voteRequestQuery.data || !voteRequests.data) { return

      no VoteRequest contractId is specified

      ; } @@ -65,7 +67,7 @@ const VoteRequestModalContent: React.FC = ({ const curSvVote: SvVote | undefined = votesQuery.data.find(v => v.voter === svPartyId); - const allVotes = votesQuery.data.sort((a, b) => { + const allVotes = [...votesQuery.data].sort((a, b) => { return b.expiresAt.valueOf() - a.expiresAt.valueOf(); }); @@ -81,9 +83,9 @@ const VoteRequestModalContent: React.FC = ({ voteBefore={dayjs(voteRequestQuery.data.payload.voteBefore).toDate()} rejectedVotes={rejectedVotes} acceptedVotes={acceptedVotes} - handleClose={handleClose} voteForm={voteForm} curSvVote={curSvVote} + effectiveAt={effectiveAt} /> ); }; diff --git a/apps/common/frontend/src/components/votes/VoteResultModalContent.tsx b/apps/common/frontend/src/components/votes/VoteResultModalContent.tsx index e03ced14..fb341015 100644 --- a/apps/common/frontend/src/components/votes/VoteResultModalContent.tsx +++ b/apps/common/frontend/src/components/votes/VoteResultModalContent.tsx @@ -10,21 +10,25 @@ import { import { ContractId } from '@daml/types'; import { SvVote } from '../../models'; +import { VoteResultModalState } from './ListVoteRequests'; import VoteModalContent from './VoteModalContent'; -interface VoteResultModalProps { - handleClose: () => void; - voteResult?: DsoRules_CloseVoteRequestResult; +interface VoteResultModalStateInterface { + voteResultModalState: VoteResultModalState; } -export const VoteResultModalContent: React.FC = ({ - handleClose, - voteResult, +export const VoteResultModalContent: React.FC = ({ + voteResultModalState, }) => { - if (!voteResult) { - return <>no voteResult defined; + if (!voteResultModalState.open) { + return <>no voteResultModalState defined; } + const { voteResult, tableType, effectiveAt } = voteResultModalState; + const encodedResult = DsoRules_CloseVoteRequestResult.encode( + voteResult + ) as DsoRules_CloseVoteRequestResult; + const allVotes = voteResult.request.votes.entriesArray(); const acceptedVotes: SvVote[] = allVotes @@ -48,16 +52,15 @@ export const VoteResultModalContent: React.FC = ({ return ( } - actionReq={voteResult.request.action} - requester={voteResult.request.requester} - reason={voteResult.request.reason} - voteBefore={dayjs(voteResult.request.voteBefore).toDate()} + voteRequestContractId={encodedResult.request.trackingCid as ContractId} + actionReq={encodedResult.request.action} + requester={encodedResult.request.requester} + reason={encodedResult.request.reason} + voteBefore={dayjs(encodedResult.request.voteBefore).toDate()} rejectedVotes={rejectedVotes} acceptedVotes={acceptedVotes} - handleClose={handleClose} + tableType={tableType} + effectiveAt={effectiveAt} /> ); }; - -export default VoteResultModalContent; diff --git a/apps/common/frontend/src/components/votes/VoteResultsFilterTable.tsx b/apps/common/frontend/src/components/votes/VoteResultsFilterTable.tsx index 04ce1a74..1ecc6b5c 100644 --- a/apps/common/frontend/src/components/votes/VoteResultsFilterTable.tsx +++ b/apps/common/frontend/src/components/votes/VoteResultsFilterTable.tsx @@ -25,6 +25,8 @@ import { DsoRules_CloseVoteRequestResult, } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules/module'; +import { VoteResultModalState } from './ListVoteRequests'; + dayjs.extend(utc); export type VoteRequestResultTableType = 'Executed' | 'Planned' | 'Rejected'; @@ -33,7 +35,7 @@ interface ListVoteResultsTableProps { getAction: (action: ActionRequiringConfirmation, staled: boolean) => string; tableBodyId: string; tableType: VoteRequestResultTableType; - openModalWithVoteResult: (voteResult: DsoRules_CloseVoteRequestResult) => void; + openModalWithVoteResult: (voteResultModalState: VoteResultModalState) => void; accepted: boolean; effectiveFrom?: string; effectiveTo?: string; @@ -47,7 +49,7 @@ type VoteRequestResultRow = { expiresAt: Date; effectiveAt: Date; idx: number; - voteResult?: DsoRules_CloseVoteRequestResult; + voteResult: DsoRules_CloseVoteRequestResult; expired: boolean; voteStatus: string[][]; }; @@ -233,13 +235,11 @@ export const VoteResultsFilterTable: React.FC = ({ tableType === 'Planned' ) { return true; - } else if ( - ['VRO_Rejected', 'VRO_Expired'].includes(result.outcome.tag) && - tableType === 'Rejected' - ) { - return true; } else { - return false; + return ( + ['VRO_Rejected', 'VRO_Expired'].includes(result.outcome.tag) && + tableType === 'Rejected' + ); } }) : []; @@ -292,7 +292,12 @@ export const VoteResultsFilterTable: React.FC = ({ } const handleRowClick: GridEventListener<'rowClick'> = (params: GridRowParams) => { - openModalWithVoteResult(params.row.voteResult); + openModalWithVoteResult({ + open: true, + voteResult: params.row.voteResult, + tableType: tableType, + effectiveAt: params.row.effectiveAt, + }); }; return ( diff --git a/apps/common/frontend/src/components/votes/VotesHooksProvider.tsx b/apps/common/frontend/src/components/votes/VotesHooksProvider.tsx index feb446a1..6bfc6fd9 100644 --- a/apps/common/frontend/src/components/votes/VotesHooksProvider.tsx +++ b/apps/common/frontend/src/components/votes/VotesHooksProvider.tsx @@ -24,7 +24,7 @@ export interface BaseVotesHooks { requester?: string, effectiveFrom?: string, effectiveTo?: string, - executed?: boolean + accepted?: boolean ) => UseQueryResult; useListVotes: (contractIds: ContractId[]) => UseQueryResult; useDsoInfos: () => UseQueryResult; diff --git a/apps/common/frontend/src/components/votes/index.ts b/apps/common/frontend/src/components/votes/index.ts index 2fd97323..f6964369 100644 --- a/apps/common/frontend/src/components/votes/index.ts +++ b/apps/common/frontend/src/components/votes/index.ts @@ -3,4 +3,5 @@ import { ListVoteRequests } from './ListVoteRequests'; import { VotesHooksContext, BaseVotesHooks, VotesHooks, useVotesHooks } from './VotesHooksProvider'; +export { ActionView } from './ActionView'; export { ListVoteRequests, VotesHooksContext, BaseVotesHooks, VotesHooks, useVotesHooks }; diff --git a/apps/common/src/main/scala/com/daml/network/http/HttpVotesHandler.scala b/apps/common/src/main/scala/com/daml/network/http/HttpVotesHandler.scala index b05e3b6e..5bde0947 100644 --- a/apps/common/src/main/scala/com/daml/network/http/HttpVotesHandler.scala +++ b/apps/common/src/main/scala/com/daml/network/http/HttpVotesHandler.scala @@ -10,6 +10,10 @@ import com.daml.network.store.{PageLimit, VotesStore} import com.digitalasset.canton.logging.NamedLogging import com.digitalasset.canton.tracing.{Spanning, TraceContext} import io.opentelemetry.api.trace.Tracer +import com.daml.network.util.Contract +import com.digitalasset.canton.daml.lf.value.json.ApiCodecCompressed +import cats.syntax.either.* +import com.digitalasset.canton.util.ErrorUtil import scala.concurrent.{ExecutionContext, Future} @@ -51,12 +55,17 @@ trait HttpVotesHandler extends Spanning with NamedLogging { } yield { definitions.ListDsoRulesVoteResultsResponse( voteResults - .map(_.toJson) - .map(json => + .map(voteResult => { io.circe.parser - .parse(json) - .getOrElse(throw new IllegalStateException(s"Failed to parse $json")) - ) + .parse( + ApiCodecCompressed + .apiValueToJsValue(Contract.javaValueToLfValue(voteResult.toValue)) + .compactPrint + ) + .valueOr(err => + ErrorUtil.invalidState(s"Failed to convert from spray to circe: $err") + ) + }) .toVector ) } diff --git a/apps/package-lock.json b/apps/package-lock.json index 04a1e11b..6c7dbd18 100644 --- a/apps/package-lock.json +++ b/apps/package-lock.json @@ -658,8 +658,11 @@ "common-typeface-termina": "1.0.0", "date-fns": "2.29.3", "decimal.js-light": "2.5.1", + "dompurify": "3.1.6", "grpc-web": "1.3.1", + "html-react-parser": "5.1.15", "jose": "4.10.3", + "jsondiffpatch": "0.6.0", "react-oidc-context": "2.2.2", "react-router-dom": "6.8.1", "splitwell-openapi": "0.0.1", @@ -671,9 +674,11 @@ "@tanstack/eslint-plugin-query": "4.29.4", "@tanstack/react-query-devtools": "4.29.6", "@trivago/prettier-plugin-sort-imports": "4.0.0", + "@types/dompurify": "3.0.5", "@types/node": "18.11.0", "@types/react": "18.0.28", "@types/react-dom": "18.0.11", + "common-test-utils": "^0.1.0", "eslint": "8.34.0", "eslint-config-react-app": "7.0.1", "prettier": "2.8.4", @@ -6585,6 +6590,20 @@ "@types/ms": "*" } }, + "node_modules/@types/diff-match-patch": { + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", + "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==" + }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -6737,6 +6756,12 @@ "@types/jest": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true + }, "node_modules/@types/url-parse": { "version": "1.4.4", "dev": true, @@ -8367,6 +8392,11 @@ "node": ">=0.4.0" } }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" + }, "node_modules/diff-sequences": { "version": "29.4.3", "dev": true, @@ -8410,6 +8440,30 @@ "csstype": "^3.0.2" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, "node_modules/domexception": { "version": "2.0.1", "dev": true, @@ -8433,6 +8487,38 @@ "node": ">=8" } }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==" + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.526", "dev": true, @@ -8443,6 +8529,17 @@ "dev": true, "license": "MIT" }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "license": "MIT", @@ -10046,17 +10143,6 @@ "whatwg-mimetype": "^3.0.0" } }, - "node_modules/happy-dom/node_modules/entities": { - "version": "4.5.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/happy-dom/node_modules/webidl-conversions": { "version": "7.0.0", "dev": true, @@ -10177,6 +10263,15 @@ "version": "16.13.1", "license": "MIT" }, + "node_modules/html-dom-parser": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-5.0.10.tgz", + "integrity": "sha512-GwArYL3V3V8yU/mLKoFF7HlLBv80BZ2Ey1BzfVNRpAci0cEKhFHI/Qh8o8oyt3qlAMLlK250wsxLdYX4viedvg==", + "dependencies": { + "domhandler": "5.0.3", + "htmlparser2": "9.1.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "2.0.1", "dev": true, @@ -10190,6 +10285,44 @@ "node": ">=10" } }, + "node_modules/html-react-parser": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-5.1.15.tgz", + "integrity": "sha512-LRwSTseAZtdtzYbBaN0a+pJ48x4qmwPzQC5tvwAp9IvuNf7afxtTHLpCPYCsVjRKRUqhXvfjTaKJJrhctxkHJA==", + "dependencies": { + "domhandler": "5.0.3", + "html-dom-parser": "5.0.10", + "react-property": "2.0.2", + "style-to-js": "1.1.13" + }, + "peerDependencies": { + "@types/react": "0.14 || 15 || 16 || 17 || 18", + "react": "0.14 || 15 || 16 || 17 || 18" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, "node_modules/http-proxy-agent": { "version": "4.0.1", "dev": true, @@ -10315,6 +10448,11 @@ "dev": true, "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.3.tgz", + "integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==" + }, "node_modules/inquirer": { "version": "8.2.6", "dev": true, @@ -11052,6 +11190,33 @@ "node": ">=6" } }, + "node_modules/jsondiffpatch": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz", + "integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==", + "dependencies": { + "@types/diff-match-patch": "^1.0.36", + "chalk": "^5.3.0", + "diff-match-patch": "^1.0.5" + }, + "bin": { + "jsondiffpatch": "bin/jsondiffpatch.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/jsondiffpatch/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.3", "dev": true, @@ -12137,6 +12302,11 @@ "react": ">=16.8.0" } }, + "node_modules/react-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.2.tgz", + "integrity": "sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==" + }, "node_modules/react-router": { "version": "6.8.1", "license": "MIT", @@ -12846,6 +13016,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.13.tgz", + "integrity": "sha512-+43kvxwjrW9n5gFR40Rv98A0/Mcjew7Lt+p5Nnw1KGR9SZf/ZaKqmMwl9Enj9EnYNcJ5VzuCjejC5KZzvH2lOA==", + "dependencies": { + "style-to-object": "1.0.6" + } + }, + "node_modules/style-to-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.6.tgz", + "integrity": "sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA==", + "dependencies": { + "inline-style-parser": "0.2.3" + } + }, "node_modules/stylis": { "version": "4.2.0", "license": "MIT" diff --git a/apps/sv/frontend/src/App.tsx b/apps/sv/frontend/src/App.tsx index ed7df3da..30434f6c 100644 --- a/apps/sv/frontend/src/App.tsx +++ b/apps/sv/frontend/src/App.tsx @@ -18,6 +18,7 @@ import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { SvAdminClientProvider } from './contexts/SvAdminServiceContext'; +import { SvAppVotesHooksProvider } from './contexts/SvAppVotesHooksContext'; import AmuletPrice from './routes/amuletPrice'; import AuthCheck from './routes/authCheck'; import Delegate from './routes/delegate'; @@ -46,9 +47,11 @@ const Providers: React.FC = ({ children }) => { navigate(path)}> - - {children} - + + + {children} + + diff --git a/apps/sv/frontend/src/__tests__/config-diffs.test.tsx b/apps/sv/frontend/src/__tests__/config-diffs.test.tsx new file mode 100644 index 00000000..f5c51a07 --- /dev/null +++ b/apps/sv/frontend/src/__tests__/config-diffs.test.tsx @@ -0,0 +1,208 @@ +// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +import { fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { + checkAmuletRulesExpectedConfigDiffsHTML, + checkDsoRulesExpectedConfigDiffsHTML, +} from 'common-test-utils'; +import { test, expect, describe } from 'vitest'; + +import App from '../App'; +import { SvConfigProvider } from '../utils'; +import { + svPartyId, + getExpectedAmuletRulesConfigDiffsHTML, + getExpectedDsoRulesConfigDiffsHTML, +} from './mocks/constants'; + +const AppWithConfig = () => { + return ( + + + + ); +}; + +describe('SV user can', () => { + // TODO(tech-debt): create unique login function + test('login and see the SV party ID', async () => { + const user = userEvent.setup(); + render(); + + const input = screen.getByRole('textbox'); + await user.type(input, 'sv1'); + + const button = screen.getByRole('button', { name: 'Log In' }); + user.click(button); + + expect(await screen.findAllByDisplayValue(svPartyId)).toBeDefined(); + }); +}); + +describe('SV can see config diffs of CRARC_AddFutureAmuletConfigSchedule', () => { + const action = 'CRARC_AddFutureAmuletConfigSchedule'; + + test('while creating a vote request.', async () => { + const user = userEvent.setup(); + render(); + + expect(await screen.findByText('Governance')).toBeDefined(); + await user.click(screen.getByText('Governance')); + + expect(await screen.findByText('Vote Requests')).toBeDefined(); + expect(await screen.findByText('Governance')).toBeDefined(); + + const dropdown = screen.getByTestId('display-actions'); + expect(dropdown).toBeDefined(); + fireEvent.change(dropdown!, { target: { value: action } }); + + expect(await screen.findByText('Config diffs')).toBeDefined(); + + // current comparison + 1 in-flight vote request + checkNumberNumberOfDiffs(2); + }); + + test('in the action needed section.', async () => { + const user = userEvent.setup(); + render(); + + await goToGovernanceTabAndClickOnAction('Action Needed', action, user); + + const mockHtmlContent = getExpectedAmuletRulesConfigDiffsHTML('4815162342', '222.2'); + checkAmuletRulesExpectedConfigDiffsHTML(mockHtmlContent, 0); + + // current comparison + checkNumberNumberOfDiffs(1); + }); + + test('in the planned section.', async () => { + const user = userEvent.setup(); + render(); + + await goToGovernanceTabAndClickOnAction('Planned', action, user); + + const mockHtmlContent = getExpectedAmuletRulesConfigDiffsHTML('4815162342', '1.03'); + checkAmuletRulesExpectedConfigDiffsHTML(mockHtmlContent, 0); + + // current comparison + checkNumberNumberOfDiffs(1); + }); + + test('in the executed section.', async () => { + const user = userEvent.setup(); + render(); + + await goToGovernanceTabAndClickOnAction('Executed', action, user); + + //TODO(#14813): when an action is executed, the AmuletConfigSchedule is updated and actualized to now, therefore the diff is empty for the first change + screen.getByTestId('stringify-display'); + + // current comparison against vote result + checkNumberNumberOfDiffs(1); + }); + + test('in the rejected section.', async () => { + const user = userEvent.setup(); + render(); + + await goToGovernanceTabAndClickOnAction('Rejected', action, user); + + screen.getByTestId('stringify-display'); + + // current comparison against vote result + checkNumberNumberOfDiffs(1); + }); +}); + +describe('SV can see config diffs of SRARC_SetConfig', () => { + const action = 'SRARC_SetConfig'; + + test('while creating a vote request.', async () => { + const user = userEvent.setup(); + render(); + + expect(await screen.findByText('Governance')).toBeDefined(); + await user.click(screen.getByText('Governance')); + + expect(await screen.findByText('Vote Requests')).toBeDefined(); + expect(await screen.findByText('Governance')).toBeDefined(); + + const dropdown = screen.getByTestId('display-actions'); + expect(dropdown).toBeDefined(); + fireEvent.change(dropdown!, { target: { value: action } }); + + const checkBox = screen.getByTestId('enable-next-scheduled-domain-upgrade'); + await user.click(checkBox); + + expect(await screen.findByText('Config diffs')).toBeDefined(); + + // current comparison + 1 in-flight vote request + checkNumberNumberOfDiffs(3); + }); + + test('in the action needed section.', async () => { + const user = userEvent.setup(); + render(); + + await goToGovernanceTabAndClickOnAction('Action Needed', action, user); + const mockHtmlContent = getExpectedDsoRulesConfigDiffsHTML('1600', '2100'); + checkDsoRulesExpectedConfigDiffsHTML(mockHtmlContent); + + // current comparison + checkNumberNumberOfDiffs(2); + }); + + test('of a SetConfig vote result in the executed section.', async () => { + const user = userEvent.setup(); + render(); + + await goToGovernanceTabAndClickOnAction('Executed', 'SRARC_SetConfig', user); + + // when an action is executed, the AmuletConfigSchedule is updated and actualized to now, therefore the diff is empty + const mockHtmlContent = getExpectedDsoRulesConfigDiffsHTML('1600', '1800'); + checkDsoRulesExpectedConfigDiffsHTML(mockHtmlContent); + + // current comparison against vote result + checkNumberNumberOfDiffs(1); + }); + + test('in the rejected section.', async () => { + const user = userEvent.setup(); + render(); + + await goToGovernanceTabAndClickOnAction('Rejected', action, user); + + const mockHtmlContent = getExpectedDsoRulesConfigDiffsHTML('1600', '2000'); + checkDsoRulesExpectedConfigDiffsHTML(mockHtmlContent); + + // current comparison against vote result + checkNumberNumberOfDiffs(1); + }); +}); + +function checkNumberNumberOfDiffs(expected: number): void { + // eslint-disable-next-line testing-library/no-node-access + const accordionElements = document.querySelectorAll( + '.MuiButtonBase-root.MuiAccordionSummary-root.MuiAccordionSummary-gutters' + ); + expect(accordionElements.length).toBe(expected); +} + +async function goToGovernanceTabAndClickOnAction( + tableType: string, + action: string, + user: ReturnType +): Promise { + expect(await screen.findByText('Governance')).toBeDefined(); + await user.click(screen.getByText('Governance')); + + expect(await screen.findByText('Vote Requests')).toBeDefined(); + expect(await screen.findByText('Governance')).toBeDefined(); + + expect(await screen.findByText(tableType)).toBeDefined(); + await user.click(screen.getByText(tableType)); + + expect(await screen.findAllByText(action)).toBeDefined(); + await user.click(screen.getAllByText(action)[0]); +} diff --git a/apps/sv/frontend/src/__tests__/mocks/constants.ts b/apps/sv/frontend/src/__tests__/mocks/constants.ts index d53c96d8..12589df8 100644 --- a/apps/sv/frontend/src/__tests__/mocks/constants.ts +++ b/apps/sv/frontend/src/__tests__/mocks/constants.ts @@ -1,13 +1,137 @@ // Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 import * as jtv from '@mojotech/json-type-validation'; -import { dsoInfo } from 'common-test-utils'; -import { ListDsoRulesVoteResultsResponse } from 'sv-openapi'; +import { dsoInfo, getAmuletConfig } from 'common-test-utils'; +import { + ListDsoRulesVoteRequestsResponse, + ListDsoRulesVoteResultsResponse, + ListVoteRequestByTrackingCidResponse, + LookupDsoRulesVoteRequestResponse, +} from 'sv-openapi'; import { AmuletRules } from '@daml.js/splice-amulet/lib/Splice/AmuletRules'; import { DsoRules } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules/module'; // Static constants for mock values +export const voteRequests: ListDsoRulesVoteRequestsResponse = { + dso_rules_vote_requests: [ + { + template_id: + '2790a114f83d5f290261fae1e7e46fba75a861a3dd603c6b4ef6b67b49053948:Splice.DsoRules:VoteRequest', + contract_id: + '10f1a2cbcd5a2dc9ad2fb9d17fec183d75de19ca91f623cbd2eaaf634e8d7cb4b5ca101220b5c5c20442f608e151ca702e0c4f51341a338c5979c0547dfcc80f911061ca99', + payload: { + dso: 'DSO::1220ebe7643fe0617f6f8e1d147137a3b174b350adf0ac2280f967c9abb712c81afb', + votes: [ + [ + 'Digital-Asset-2', + { + sv: 'digital-asset-2::122063072c8e53ca2690deeff0be9002ac252f9927caebec8e2f64233b95db66da38', + accept: true, + reason: { + url: '', + body: 'I accept, as I requested the vote.', + }, + }, + ], + ], + voteBefore: '2038-09-11T10:27:52.300591Z', + requester: 'Digital-Asset-2', + reason: { + url: '', + body: 'df', + }, + trackingCid: null, + action: getAmuletRulesAction( + 'CRARC_AddFutureAmuletConfigSchedule', + '2042-09-11T10:27:00Z', + '222.2' + ), + }, + created_event_blob: + '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', + created_at: '2024-09-11T10:28:09.304591Z', + }, + { + template_id: + '2790a114f83d5f290261fae1e7e46fba75a861a3dd603c6b4ef6b67b49053948:Splice.DsoRules:VoteRequest', + contract_id: + '20f1a2cbcd5a2dc9ad2fb9d17fec183d75de19ca91f623cbd2eaaf634e8d7cb4b5ca101220b5c5c20442f608e151ca702e0c4f51341a338c5979c0547dfcc80f911061ca99', + payload: { + dso: 'DSO::1220ebe7643fe0617f6f8e1d147137a3b174b350adf0ac2280f967c9abb712c81afb', + votes: [ + [ + 'Digital-Asset-2', + { + sv: 'digital-asset-2::122063072c8e53ca2690deeff0be9002ac252f9927caebec8e2f64233b95db66da38', + accept: true, + reason: { + url: '', + body: 'I accept, as I requested the vote.', + }, + }, + ], + ], + voteBefore: '2028-09-11T10:27:52.300591Z', + requester: 'Digital-Asset-2', + reason: { + url: '', + body: 'df', + }, + trackingCid: null, + action: getDsoAction('2100'), + }, + created_event_blob: + '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', + created_at: '2024-09-11T10:28:09.304591Z', + }, + { + template_id: + '2790a114f83d5f290261fae1e7e46fba75a861a3dd603c6b4ef6b67b49053948:Splice.DsoRules:VoteRequest', + contract_id: + '20f1a2cbcd5a2dc9ad2fb9d17fec183d75de19ca91f623cbd2eaaf634e8d7cb4b5ca101220b5c5c20442f608e151ca702e0c4f51341a338c5979c0547dfcc80f911061ca99', + payload: { + dso: 'DSO::1220ebe7643fe0617f6f8e1d147137a3b174b350adf0ac2280f967c9abb712c81afb', + votes: [ + [ + 'Digital-Asset-2', + { + sv: 'digital-asset-2::122063072c8e53ca2690deeff0be9002ac252f9927caebec8e2f64233b95db66da38', + accept: true, + reason: { + url: '', + body: 'I accept, as I requested the vote.', + }, + }, + ], + ], + voteBefore: '2048-09-11T10:27:52.300591Z', + requester: 'Digital-Asset-2', + reason: { + url: '', + body: 'df', + }, + trackingCid: null, + action: getDsoAction('400'), + }, + created_event_blob: + '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', + created_at: '2024-02-11T10:28:09.304591Z', + }, + ], +}; + +export const voteRequest: ListVoteRequestByTrackingCidResponse = { + vote_requests: voteRequests.dso_rules_vote_requests, +}; + +export const voteRequestsAddFutureConfig: LookupDsoRulesVoteRequestResponse = { + dso_rules_vote_request: voteRequests.dso_rules_vote_requests[0], +}; + +export const voteRequestsSetConfig: LookupDsoRulesVoteRequestResponse = { + dso_rules_vote_request: voteRequests.dso_rules_vote_requests[1], +}; export const voteResults: ListDsoRulesVoteResultsResponse = { dso_rules_vote_results: [ @@ -15,156 +139,11 @@ export const voteResults: ListDsoRulesVoteResultsResponse = { request: { dso: 'DSO::122013fe9b84dfc756163484f071da40f5605c2ab9d93eb647052c15e360acce1347', requester: 'Digital-Asset-2', - action: { - tag: 'ARC_AmuletRules', - value: { - amuletRulesAction: { - tag: 'CRARC_AddFutureAmuletConfigSchedule', - value: { - newScheduleItem: { - _1: '2024-04-20T08:30:00Z', - _2: { - transferConfig: { - createFee: { - fee: '110.0300000000', - }, - holdingFee: { - rate: '0.0000048225', - }, - transferFee: { - initialRate: '0.0100000000', - steps: [ - { - _1: '100.0000000000', - _2: '0.0010000000', - }, - { - _1: '1000.0000000000', - _2: '0.0001000000', - }, - { - _1: '1000000.0000000000', - _2: '0.0000100000', - }, - ], - }, - lockHolderFee: { - fee: '0.0050000000', - }, - extraFeaturedAppRewardAmount: '1.0000000000', - maxNumInputs: '100', - maxNumOutputs: '100', - maxNumLockHolders: '50', - }, - issuanceCurve: { - initialValue: { - amuletToIssuePerYear: '40000000000.0000000000', - validatorRewardPercentage: '0.0500000000', - appRewardPercentage: '0.1500000000', - validatorRewardCap: '0.2000000000', - featuredAppRewardCap: '100.0000000000', - unfeaturedAppRewardCap: '0.6000000000', - optValidatorFaucetCap: '2.8500000000', - }, - futureValues: [ - { - _1: { - microseconds: '15768000000000', - }, - _2: { - amuletToIssuePerYear: '20000000000.0000000000', - validatorRewardPercentage: '0.1200000000', - appRewardPercentage: '0.4000000000', - validatorRewardCap: '0.2000000000', - featuredAppRewardCap: '100.0000000000', - unfeaturedAppRewardCap: '0.6000000000', - optValidatorFaucetCap: '2.8500000000', - }, - }, - { - _1: { - microseconds: '47304000000000', - }, - _2: { - amuletToIssuePerYear: '10000000000.0000000000', - validatorRewardPercentage: '0.1800000000', - appRewardPercentage: '0.6200000000', - validatorRewardCap: '0.2000000000', - featuredAppRewardCap: '100.0000000000', - unfeaturedAppRewardCap: '0.6000000000', - optValidatorFaucetCap: '2.8500000000', - }, - }, - { - _1: { - microseconds: '157680000000000', - }, - _2: { - amuletToIssuePerYear: '5000000000.0000000000', - validatorRewardPercentage: '0.2100000000', - appRewardPercentage: '0.6900000000', - validatorRewardCap: '0.2000000000', - featuredAppRewardCap: '100.0000000000', - unfeaturedAppRewardCap: '0.6000000000', - optValidatorFaucetCap: '2.8500000000', - }, - }, - { - _1: { - microseconds: '315360000000000', - }, - _2: { - amuletToIssuePerYear: '2500000000.0000000000', - validatorRewardPercentage: '0.2000000000', - appRewardPercentage: '0.7500000000', - validatorRewardCap: '0.2000000000', - featuredAppRewardCap: '100.0000000000', - unfeaturedAppRewardCap: '0.6000000000', - optValidatorFaucetCap: '2.8500000000', - }, - }, - ], - }, - decentralizedSynchronizer: { - requiredSynchronizers: { - map: [ - [ - 'global-domain::122013fe9b84dfc756163484f071da40f5605c2ab9d93eb647052c15e360acce1347', - {}, - ], - ], - }, - activeSynchronizer: - 'global-domain::122013fe9b84dfc756163484f071da40f5605c2ab9d93eb647052c15e360acce1347', - fees: { - baseRateTrafficLimits: { - burstAmount: '2000000', - burstWindow: { - microseconds: '600000000', - }, - }, - extraTrafficPrice: '1.0000000000', - readVsWriteScalingFactor: '4', - minTopupAmount: '10000000', - }, - }, - tickDuration: { - microseconds: '150000000', - }, - packageConfig: { - amulet: '0.1.0', - amuletNameService: '0.1.0', - dsoGovernance: '0.1.0', - validatorLifecycle: '0.1.0', - wallet: '0.1.0', - walletPayments: '0.1.0', - }, - }, - }, - }, - }, - }, - }, + action: getAmuletRulesAction( + 'CRARC_AddFutureAmuletConfigSchedule', + '2024-03-15T08:35:00Z', + '4815162342' + ), reason: { url: '', body: 'ads', @@ -174,7 +153,7 @@ export const voteResults: ListDsoRulesVoteResultsResponse = { [ 'Digital-Asset-2', { - sv: 'digital-asset-2::122045496d88b854f49ce75e9032e73ee8157d14f7d05ecbdc8895b566f409e68fe8', + sv: 'digital-asset-2::122063072c8e53ca2690deeff0be9002ac252f9927caebec8e2f64233b95db66da38', accept: true, reason: { url: '', @@ -195,10 +174,387 @@ export const voteResults: ListDsoRulesVoteResultsResponse = { }, }, }, + { + request: { + dso: 'DSO::122013fe9b84dfc756163484f071da40f5605c2ab9d93eb647052c15e360acce1347', + requester: 'Digital-Asset-2', + action: getAmuletRulesAction( + 'CRARC_AddFutureAmuletConfigSchedule', + '2024-03-15T08:35:00Z', + '4815162342' + ), + reason: { + url: '', + body: 'ads', + }, + voteBefore: '2024-04-19T08:15:11.839403Z', + votes: [ + [ + 'Digital-Asset-2', + { + sv: 'digital-asset-2::122063072c8e53ca2690deeff0be9002ac252f9927caebec8e2f64233b95db66da38', + accept: false, + reason: { + url: '', + body: 'I refuse.', + }, + }, + ], + ], + trackingCid: null, + }, + completedAt: '2024-04-20T08:21:26.130819Z', + offboardedVoters: [], + abstainingSvs: [], + outcome: { + tag: 'VRO_Rejected', + value: { + effectiveAt: '2024-04-20T08:30:00Z', + }, + }, + }, + { + request: { + dso: 'DSO::12200c1f141acd0b2e48defae40aa2eb3daae48e4c16b7e1fa5d9211d352cc150c81', + votes: [ + [ + 'Digital-Asset-2', + { + sv: 'digital-asset-2::122063072c8e53ca2690deeff0be9002ac252f9927caebec8e2f64233b95db66da38', + accept: true, + reason: { + url: '', + body: 'I accept, as I requested the vote.', + }, + }, + ], + ], + voteBefore: '2041-09-10T13:53:11.947419Z', + requester: 'Digital-Asset-2', + reason: { + url: '', + body: 'asdf', + }, + trackingCid: null, + action: getAmuletRulesAction( + 'CRARC_AddFutureAmuletConfigSchedule', + '2042-09-20T17:53:00Z', + '1.03' + ), + }, + completedAt: '2024-09-10T13:53:37.031740Z', + abstainingSvs: [], + outcome: { + tag: 'VRO_Accepted', + value: { + effectiveAt: '2042-09-20T17:53:00Z', + }, + }, + offboardedVoters: [], + }, + { + request: { + dso: 'DSO::1220d57d4ce92ad14bb5647b453f2ba69c721e69810ca7d376d2c1455323a6763c37', + votes: [ + [ + 'Digital-Asset-2', + { + sv: 'digital-asset-2::122063072c8e53ca2690deeff0be9002ac252f9927caebec8e2f64233b95db66da38', + accept: true, + reason: { + url: '', + body: 'I accept, as I requested the vote.', + }, + }, + ], + ], + voteBefore: '2024-09-09T20:10:01.157168Z', + requester: 'Digital-Asset-2', + reason: { + url: '', + body: 'd', + }, + trackingCid: null, + action: getDsoAction('1800'), + }, + completedAt: '2024-09-10T16:10:10.253341Z', + abstainingSvs: [], + outcome: { + tag: 'VRO_Accepted', + value: { + effectiveAt: '2024-09-10T16:10:10.253341Z', + }, + }, + offboardedVoters: [], + }, + { + request: { + dso: 'DSO::1220d57d4ce92ad14bb5647b453f2ba69c721e69810ca7d376d2c1455323a6763c37', + votes: [ + [ + 'Digital-Asset-2', + { + sv: 'digital-asset-2::122063072c8e53ca2690deeff0be9002ac252f9927caebec8e2f64233b95db66da38', + accept: false, + reason: { + url: '', + body: 'I refuse.', + }, + }, + ], + ], + voteBefore: '2024-08-10T20:10:01.157168Z', + requester: 'Digital-Asset-2', + reason: { + url: '', + body: 'd', + }, + trackingCid: null, + action: getDsoAction('2000'), + }, + completedAt: '2024-08-11T11:10:10.253341Z', + abstainingSvs: [], + outcome: { + tag: 'VRO_Rejected', + value: { + effectiveAt: '2024-08-11T11:10:10.253341Z', + }, + }, + offboardedVoters: [], + }, ], }; -// Sanity check / guard against template changes +function getAmuletRulesAction(action: string, effectiveAt: string, createFee: string) { + return { + tag: 'ARC_AmuletRules', + value: { + amuletRulesAction: { + tag: action, + value: { + newScheduleItem: { + _1: effectiveAt, + _2: getAmuletConfig(createFee), + }, + }, + }, + }, + }; +} + +function getDsoAction(acsCommitmentReconciliationInterval: string) { + return { + tag: 'ARC_DsoRules', + value: { + dsoAction: { + tag: 'SRARC_SetConfig', + value: { + newConfig: { + numMemberTrafficContractsThreshold: '5', + dsoDelegateInactiveTimeout: { + microseconds: '70000000', + }, + svOnboardingRequestTimeout: { + microseconds: '3600000000', + }, + nextScheduledSynchronizerUpgrade: null, + actionConfirmationTimeout: { + microseconds: '3600000000', + }, + maxTextLength: '1024', + voteRequestTimeout: { + microseconds: '604800000000', + }, + decentralizedSynchronizer: { + synchronizers: [ + [ + 'global-domain::1220d57d4ce92ad14bb5647b453f2ba69c721e69810ca7d376d2c1455323a6763c37', + { + state: 'DS_Operational', + cometBftGenesisJson: + 'TODO(#4900): share CometBFT genesis.json of sv1 via DsoRules config.', + acsCommitmentReconciliationInterval: acsCommitmentReconciliationInterval, + }, + ], + ], + lastSynchronizerId: + 'global-domain::1220d57d4ce92ad14bb5647b453f2ba69c721e69810ca7d376d2c1455323a6763c37', + activeSynchronizerId: + 'global-domain::1220d57d4ce92ad14bb5647b453f2ba69c721e69810ca7d376d2c1455323a6763c37', + }, + numUnclaimedRewardsThreshold: '4410', + svOnboardingConfirmedTimeout: { + microseconds: '3600000000', + }, + synchronizerNodeConfigLimits: { + cometBft: { + maxNumSequencingKeys: '2', + maxNodeIdLength: '50', + maxNumCometBftNodes: '2', + maxPubKeyLength: '256', + maxNumGovernanceKeys: '2', + }, + }, + }, + }, + }, + }, + }; +} + +export function getExpectedDsoRulesConfigDiffsHTML( + originalAcsCommitmentReconciliationInterval: string, + replacementAcsCommitmentReconciliationInterval: string +): string { + return ( + '
      • actionConfirmationTimeout
        {\n' +
        +    '  "microseconds": "3600000000"\n' +
        +    `}
      • decentralizedSynchronizer
        • activeSynchronizerId
          • 486
            bal-synchronizer::1220a555ecceed7fef445c7ec333c14449d981fb6595be218c5d701eef5ea63a1bcadomain::1220d57d4ce92ad14bb5647b453f2ba69c721e69810ca7d376d2c1455323a6763c37
        • lastSynchronizerId
          • 486
            bal-synchronizer::1220a555ecceed7fef445c7ec333c14449d981fb6595be218c5d701eef5ea63a1bcadomain::1220d57d4ce92ad14bb5647b453f2ba69c721e69810ca7d376d2c1455323a6763c37
          • synchronizers
            • 0
              • 0
                "global-synchronizer::1220a555ecceed7fef445c7ec333c14449d981fb6595be218c5d701eef5ea63a1bca"
              • 0
                "global-domain::1220d57d4ce92ad14bb5647b453f2ba69c721e69810ca7d376d2c1455323a6763c37"
              • 1
                • acsCommitmentReconciliationInterval
                  "${originalAcsCommitmentReconciliationInterval}"
                  "${replacementAcsCommitmentReconciliationInterval}"
                • cometBftGenesisJson
                  • 4124
                    of founding SV nodesv1 via
                  • state
                    "DS_Operational"
                  • dsoDelegateInactiveTimeout
                    {\n` +
                    +    '  "microseconds": "70000000"\n' +
                    +    '}
                  • maxTextLength
                    "1024"
                  • nextScheduledSynchronizerUpgrade
                    null
                  • numMemberTrafficContractsThreshold
                    "5"
                  • numUnclaimedRewardsThreshold
                    "10"
                    "4410"
                  • svOnboardingConfirmedTimeout
                    {\n' +
                    +    '  "microseconds": "3600000000"\n' +
                    +    '}
                  • svOnboardingRequestTimeout
                    {\n' +
                    +    '  "microseconds": "3600000000"\n' +
                    +    '}
                  • synchronizerNodeConfigLimits
                    {\n' +
                    +    '  "cometBft": {\n' +
                    +    '    "maxNumCometBftNodes": "2",\n' +
                    +    '    "maxNumGovernanceKeys": "2",\n' +
                    +    '    "maxNumSequencingKeys": "2",\n' +
                    +    '    "maxNodeIdLength": "50",\n' +
                    +    '    "maxPubKeyLength": "256"\n' +
                    +    '  }\n' +
                    +    '}
                  • voteRequestTimeout
                    {\n' +
                    +    '  "microseconds": "604800000000"\n' +
                    +    '}
                  • \n'.trim() + ); +} + +export function getExpectedAmuletRulesConfigDiffsHTML( + originalCreateFee: string, + replacementCreateFee: string +): string { + return ( + '
                    • decentralizedSynchronizer
                      {\n' +
                      +    '  "requiredSynchronizers": {\n' +
                      +    '    "map": [\n' +
                      +    '      [\n' +
                      +    '        "global-domain::12200c1f141acd0b2e48defae40aa2eb3daae48e4c16b7e1fa5d9211d352cc150c81",\n' +
                      +    '        {}\n' +
                      +    '      ]\n' +
                      +    '    ]\n' +
                      +    '  },\n' +
                      +    '  "activeSynchronizer": "global-domain::12200c1f141acd0b2e48defae40aa2eb3daae48e4c16b7e1fa5d9211d352cc150c81",\n' +
                      +    '  "fees": {\n' +
                      +    '    "baseRateTrafficLimits": {\n' +
                      +    '      "burstAmount": "400000",\n' +
                      +    '      "burstWindow": {\n' +
                      +    '        "microseconds": "1200000000"\n' +
                      +    '      }\n' +
                      +    '    },\n' +
                      +    '    "extraTrafficPrice": "16.67",\n' +
                      +    '    "readVsWriteScalingFactor": "4",\n' +
                      +    '    "minTopupAmount": "200000"\n' +
                      +    '  }\n' +
                      +    '}
                    • issuanceCurve
                      {\n' +
                      +    '  "initialValue": {\n' +
                      +    '    "amuletToIssuePerYear": "40000000000.0",\n' +
                      +    '    "validatorRewardPercentage": "0.05",\n' +
                      +    '    "appRewardPercentage": "0.15",\n' +
                      +    '    "validatorRewardCap": "0.2",\n' +
                      +    '    "featuredAppRewardCap": "100.0",\n' +
                      +    '    "unfeaturedAppRewardCap": "0.6",\n' +
                      +    '    "optValidatorFaucetCap": "2.85"\n' +
                      +    '  },\n' +
                      +    '  "futureValues": [\n' +
                      +    '    {\n' +
                      +    '      "_1": {\n' +
                      +    '        "microseconds": "15768000000000"\n' +
                      +    '      },\n' +
                      +    '      "_2": {\n' +
                      +    '        "amuletToIssuePerYear": "20000000000.0",\n' +
                      +    '        "validatorRewardPercentage": "0.12",\n' +
                      +    '        "appRewardPercentage": "0.4",\n' +
                      +    '        "validatorRewardCap": "0.2",\n' +
                      +    '        "featuredAppRewardCap": "100.0",\n' +
                      +    '        "unfeaturedAppRewardCap": "0.6",\n' +
                      +    '        "optValidatorFaucetCap": "2.85"\n' +
                      +    '      }\n' +
                      +    '    },\n' +
                      +    '    {\n' +
                      +    '      "_1": {\n' +
                      +    '        "microseconds": "47304000000000"\n' +
                      +    '      },\n' +
                      +    '      "_2": {\n' +
                      +    '        "amuletToIssuePerYear": "10000000000.0",\n' +
                      +    '        "validatorRewardPercentage": "0.18",\n' +
                      +    '        "appRewardPercentage": "0.62",\n' +
                      +    '        "validatorRewardCap": "0.2",\n' +
                      +    '        "featuredAppRewardCap": "100.0",\n' +
                      +    '        "unfeaturedAppRewardCap": "0.6",\n' +
                      +    '        "optValidatorFaucetCap": "2.85"\n' +
                      +    '      }\n' +
                      +    '    },\n' +
                      +    '    {\n' +
                      +    '      "_1": {\n' +
                      +    '        "microseconds": "157680000000000"\n' +
                      +    '      },\n' +
                      +    '      "_2": {\n' +
                      +    '        "amuletToIssuePerYear": "5000000000.0",\n' +
                      +    '        "validatorRewardPercentage": "0.21",\n' +
                      +    '        "appRewardPercentage": "0.69",\n' +
                      +    '        "validatorRewardCap": "0.2",\n' +
                      +    '        "featuredAppRewardCap": "100.0",\n' +
                      +    '        "unfeaturedAppRewardCap": "0.6",\n' +
                      +    '        "optValidatorFaucetCap": "2.85"\n' +
                      +    '      }\n' +
                      +    '    },\n' +
                      +    '    {\n' +
                      +    '      "_1": {\n' +
                      +    '        "microseconds": "315360000000000"\n' +
                      +    '      },\n' +
                      +    '      "_2": {\n' +
                      +    '        "amuletToIssuePerYear": "2500000000.0",\n' +
                      +    '        "validatorRewardPercentage": "0.2",\n' +
                      +    '        "appRewardPercentage": "0.75",\n' +
                      +    '        "validatorRewardCap": "0.2",\n' +
                      +    '        "featuredAppRewardCap": "100.0",\n' +
                      +    '        "unfeaturedAppRewardCap": "0.6",\n' +
                      +    '        "optValidatorFaucetCap": "2.85"\n' +
                      +    '      }\n' +
                      +    '    }\n' +
                      +    '  ]\n' +
                      +    '}
                    • packageConfig
                      {\n' +
                      +    '  "amulet": "0.1.5",\n' +
                      +    '  "amuletNameService": "0.1.5",\n' +
                      +    '  "dsoGovernance": "0.1.8",\n' +
                      +    '  "validatorLifecycle": "0.1.1",\n' +
                      +    '  "wallet": "0.1.5",\n' +
                      +    '  "walletPayments": "0.1.5"\n' +
                      +    '}
                    • tickDuration
                      {\n' +
                      +    '  "microseconds": "600000000"\n' +
                      +    `}
                    • transferConfig
                      • createFee
                        • fee
                          "${originalCreateFee}"
                          "${replacementCreateFee}"
                      • extraFeaturedAppRewardAmount
                        "1.0"
                      • holdingFee
                        {\n` +
                        +    '  "rate": "0.0000190259"\n' +
                        +    '}
                      • lockHolderFee
                        {\n' +
                        +    '  "fee": "0.005"\n' +
                        +    '}
                      • maxNumInputs
                        "100"
                      • maxNumLockHolders
                        "50"
                      • maxNumOutputs
                        "100"
                      • transferFee
                        {\n' +
                        +    '  "initialRate": "0.01",\n' +
                        +    '  "steps": [\n' +
                        +    '    {\n' +
                        +    '      "_1": "100.0",\n' +
                        +    '      "_2": "0.001"\n' +
                        +    '    },\n' +
                        +    '    {\n' +
                        +    '      "_1": "1000.0",\n' +
                        +    '      "_2": "0.0001"\n' +
                        +    '    },\n' +
                        +    '    {\n' +
                        +    '      "_1": "1000000.0",\n' +
                        +    '      "_2": "0.00001"\n' +
                        +    '    }\n' +
                        +    '  ]\n' +
                        +    '}
                      • \n'.trim() + ); +} const result = jtv.Result.andThen( () => AmuletRules.decoder.run(dsoInfo.amulet_rules.contract.payload), diff --git a/apps/sv/frontend/src/__tests__/mocks/handlers/sv-api.ts b/apps/sv/frontend/src/__tests__/mocks/handlers/sv-api.ts index 7d9cd091..0b554fa0 100644 --- a/apps/sv/frontend/src/__tests__/mocks/handlers/sv-api.ts +++ b/apps/sv/frontend/src/__tests__/mocks/handlers/sv-api.ts @@ -6,9 +6,11 @@ import { ErrorResponse, ListDsoRulesVoteRequestsResponse, ListDsoRulesVoteResultsResponse, + ListVoteRequestByTrackingCidResponse, + LookupDsoRulesVoteRequestResponse, } from 'sv-openapi'; -import { voteResults } from '../constants'; +import { voteRequest, voteRequests, voteResults } from '../constants'; export const buildSvMock = (svUrl: string): RestHandler[] => [ rest.get(`${svUrl}/v0/admin/authorization`, (_, res, ctx) => { @@ -16,15 +18,28 @@ export const buildSvMock = (svUrl: string): RestHandler[] => [ }), dsoInfoHandler(svUrl), rest.get(`${svUrl}/v0/admin/sv/voterequests`, (_, res, ctx) => { + return res(ctx.json(voteRequests)); + }), + rest.get(`${svUrl}/v0/admin/sv/voterequests/:id`, (req, res, ctx) => { + const { id } = req.params; return res( - ctx.json({ - dso_rules_vote_requests: [], + ctx.json({ + dso_rules_vote_request: voteRequests.dso_rules_vote_requests.filter( + vr => vr.contract_id === id + )[0], }) ); }), - rest.post(`${svUrl}/v0/admin/sv/voteresults`, (_, res, ctx) => { - console.log(voteResults); - return res(ctx.json(voteResults)); + rest.post(`${svUrl}/v0/admin/sv/voterequest`, (_, res, ctx) => { + return res(ctx.json(voteRequest)); + }), + rest.post(`${svUrl}/v0/admin/sv/voteresults`, (req, res, ctx) => { + return req.json().then(data => { + if (data.actionName === 'SRARC_SetConfig') { + return res(ctx.json({ dso_rules_vote_results: [] })); + } + return res(ctx.json(voteResults)); + }); }), rest.get(`${svUrl}/v0/admin/domain/cometbft/debug`, (_, res, ctx) => { return res( diff --git a/apps/sv/frontend/src/__tests__/sv.test.tsx b/apps/sv/frontend/src/__tests__/sv.test.tsx index 070a6eaa..c79c73b2 100644 --- a/apps/sv/frontend/src/__tests__/sv.test.tsx +++ b/apps/sv/frontend/src/__tests__/sv.test.tsx @@ -61,7 +61,6 @@ describe('SV user can', () => { const checkBox = screen.getByTestId('enable-next-scheduled-domain-upgrade'); await user.click(checkBox); - expect(screen.queryByText('nextScheduledSynchronizerUpgrade')).toBeNull(); expect(await screen.findByText('nextScheduledSynchronizerUpgrade.time')).toBeDefined(); }); }); diff --git a/apps/sv/frontend/src/components/votes/SvListVoteRequests.tsx b/apps/sv/frontend/src/components/votes/SvListVoteRequests.tsx index 7294b0e1..c7251e08 100644 --- a/apps/sv/frontend/src/components/votes/SvListVoteRequests.tsx +++ b/apps/sv/frontend/src/components/votes/SvListVoteRequests.tsx @@ -7,7 +7,6 @@ import React from 'react'; import { VoteRequest } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules'; import { ContractId } from '@daml/types'; -import { SvAppVotesHooksProvider } from '../../contexts/SvAppVotesHooksContext'; import { useSvConfig } from '../../utils'; import VoteForm from './VoteForm'; @@ -15,14 +14,12 @@ const SvListVoteRequests: React.FC = () => { const config = useSvConfig(); return ( - - , currentSvVote?: SvVote) => ( - - )} - /> - + , currentSvVote?: SvVote) => ( + + )} + /> ); }; diff --git a/apps/sv/frontend/src/components/votes/VoteRequest.tsx b/apps/sv/frontend/src/components/votes/VoteRequest.tsx index f4f1523b..482261a2 100644 --- a/apps/sv/frontend/src/components/votes/VoteRequest.tsx +++ b/apps/sv/frontend/src/components/votes/VoteRequest.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { DecoderError } from '@mojotech/json-type-validation/dist/types/decoder'; import { useMutation } from '@tanstack/react-query'; -import { DisableConditionally, SvClientProvider } from 'common-frontend'; +import { ActionView, DisableConditionally, SvClientProvider } from 'common-frontend'; import { getUTCWithOffset } from 'common-frontend-utils'; import { Dayjs } from 'dayjs'; import dayjs from 'dayjs'; @@ -55,7 +55,7 @@ export function actionFromFormIsError( return !!(action as { formError: DecoderError }).formError; } -const VoteRequest: React.FC = () => { +export const CreateVoteRequest: React.FC = () => { // States related to vote requests const [actionName, setActionName] = useState('SRARC_OffboardSv'); const [summary, setSummary] = useState(''); @@ -121,7 +121,12 @@ const VoteRequest: React.FC = () => { const [action, setAction] = useState(undefined); const chooseAction = useCallback( (action: ActionFromForm) => { - setAction(action); + try { + ActionRequiringConfirmation.encode(action as ActionRequiringConfirmation); + setAction(action); + } catch (error) { + console.log('Caught expected DecoderError in case of null values: ', error); + } const max = (time1: Dayjs, time2: Dayjs) => (time1 > time2 ? time1 : time2); if (!actionFromFormIsError(action)) { if (action.tag === 'ARC_AmuletRules') { @@ -223,6 +228,14 @@ const VoteRequest: React.FC = () => { }, }); + // used and valid only for dsoRules-based actions + let expiresAt; + try { + expiresAt = expiration?.toISOString(); + } catch (error) { + expiresAt = undefined; + } + // TODO (#4966): add a popup to ask confirmation return ( @@ -319,33 +332,49 @@ const VoteRequest: React.FC = () => { closeOnSelect /> + {action && ( + + Review vote request + + + )} - - - + + +
                        @@ -354,9 +383,10 @@ const VoteRequest: React.FC = () => { const VoteRequestWithContexts: React.FC = () => { const config = useSvConfig(); + return ( - + ); diff --git a/apps/sv/frontend/src/components/votes/actions/AddFutureAmuletConfigSchedule.tsx b/apps/sv/frontend/src/components/votes/actions/AddFutureAmuletConfigSchedule.tsx index f50afdbd..80015d2a 100644 --- a/apps/sv/frontend/src/components/votes/actions/AddFutureAmuletConfigSchedule.tsx +++ b/apps/sv/frontend/src/components/votes/actions/AddFutureAmuletConfigSchedule.tsx @@ -87,7 +87,15 @@ const AddFutureAmuletConfigSchedule: React.FC<{ value={date} minDateTime={dayjs()} readOnly={false} - onChange={(newValue: Dayjs | null) => setDate(newValue)} + onChange={(newValue: Dayjs | null) => { + try { + newValue?.toISOString(); + setDate(newValue); + } catch (error) { + console.log('Invalid date', error); + return; + } + }} slotProps={{ textField: { id: 'datetime-picker-amulet-configuration', diff --git a/apps/sv/frontend/src/contexts/SvAppVotesHooksContext.tsx b/apps/sv/frontend/src/contexts/SvAppVotesHooksContext.tsx index 3c935285..b2f1428e 100644 --- a/apps/sv/frontend/src/contexts/SvAppVotesHooksContext.tsx +++ b/apps/sv/frontend/src/contexts/SvAppVotesHooksContext.tsx @@ -29,7 +29,7 @@ export const SvAppVotesHooksProvider: React.FC = ({ chi requester: string | undefined, effectiveFrom: string | undefined, effectiveTo: string | undefined, - executed: boolean | undefined + accepted: boolean | undefined ): UseQueryResult { return svHooks.useListVoteRequestResult( { @@ -37,7 +37,7 @@ export const SvAppVotesHooksProvider: React.FC = ({ chi requester, effectiveFrom, effectiveTo, - executed, + accepted, }, limit ); diff --git a/apps/sv/frontend/src/hooks/useListVoteRequests.tsx b/apps/sv/frontend/src/hooks/useListVoteRequests.tsx index 37d81021..6cb8ddcc 100644 --- a/apps/sv/frontend/src/hooks/useListVoteRequests.tsx +++ b/apps/sv/frontend/src/hooks/useListVoteRequests.tsx @@ -13,7 +13,7 @@ import { useSvAdminClient } from '../contexts/SvAdminServiceContext'; export type ListVoteRequestResultParams = { actionName?: string; - executed?: boolean; + accepted?: boolean; requester?: string; effectiveFrom?: string; effectiveTo?: string; @@ -43,7 +43,7 @@ export const useListVoteRequestResult = ( DsoRules_CloseVoteRequestResult, limit, query.actionName, - query.executed, + query.accepted, query.requester, query.effectiveFrom, query.effectiveTo, @@ -56,7 +56,7 @@ export const useListVoteRequestResult = ( query.requester, query.effectiveFrom, query.effectiveTo, - query.executed + query.accepted ); return List(DsoRules_CloseVoteRequestResult).decoder.runWithException(dso_rules_vote_results); }, diff --git a/build.sbt b/build.sbt index 6c53dc1a..50b7f3b9 100644 --- a/build.sbt +++ b/build.sbt @@ -732,19 +732,19 @@ lazy val `apps-common-frontend` = { BuildCommon.TS.runWorkspaceCommand( npmRootDir.value, "build", - "common-frontend", + "common-test-utils", log, ) BuildCommon.TS.runWorkspaceCommand( npmRootDir.value, "build", - "common-test-vite-utils", + "common-frontend", log, ) BuildCommon.TS.runWorkspaceCommand( npmRootDir.value, "build", - "common-test-utils", + "common-test-vite-utils", log, ) (baseDirectory.value / "lib" ** "*").get.toSet @@ -781,8 +781,8 @@ lazy val `apps-common-frontend` = { workspace <- Seq( "common-test-vite-utils", "common-frontend-utils", - "common-frontend", "common-test-utils", + "common-frontend", ) ) BuildCommon.TS.runWorkspaceCommand(npmRootDir.value, "build", workspace, log)