Skip to content

Commit 19ff6e8

Browse files
committed
Support for granting ValidatorLicense via SV UI
* Add weight, kind to ValidatorReceivedFaucets * add column in 'dso_acs_store'; 'validator_liveness_weight' numeric * Show 'Weight' and 'Operator' in Validator Licenses table PR reviewed in #2905 CI run #3209 Signed-off-by: Divam <dfordivam@gmail.com>
1 parent a5157b2 commit 19ff6e8

File tree

25 files changed

+697
-112
lines changed

25 files changed

+697
-112
lines changed

apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/SvAppReference.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,14 @@ class SvAppBackendReference(
353353
)
354354
}
355355

356+
@Help.Summary("Grant a ValidatorLicense to a validator party (via admin API)")
357+
def grantValidatorLicense(partyId: PartyId): Unit =
358+
consoleEnvironment.run {
359+
httpCommand(
360+
HttpSvAdminAppClient.GrantValidatorLicense(partyId)
361+
)
362+
}
363+
356364
@Help.Summary("Update CC price vote (via admin API)")
357365
def updateAmuletPriceVote(amuletPrice: BigDecimal): Unit =
358366
consoleEnvironment.run {

apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvFrontendIntegrationTest.scala

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,56 @@ class SvFrontendIntegrationTest
162162
}
163163
}
164164

165+
"can grant a validator license to an existing party" in { implicit env =>
166+
withFrontEnd("sv1") { implicit webDriver =>
167+
actAndCheck(
168+
"sv1 operator can login and browse to the validator-onboarding tab", {
169+
go to s"http://localhost:$sv1UIPort/validator-onboarding"
170+
loginOnCurrentPage(sv1UIPort, sv1Backend.config.ledgerApiUser)
171+
},
172+
)(
173+
"We see the grant validator license form",
174+
_ => {
175+
find(id("grant-license-party-address")) should not be empty
176+
find(id("grant-validator-license")) should not be empty
177+
},
178+
)
179+
180+
val licenseRows = getLicensesTableRows
181+
val newValidatorParty = allocateRandomSvParty("test-validator", Some(100))
182+
183+
actAndCheck(
184+
"fill party address", {
185+
inside(find(id("grant-license-party-address"))) { case Some(element) =>
186+
element.underlying.sendKeys(newValidatorParty.toProtoPrimitive)
187+
}
188+
},
189+
)(
190+
"grant button becomes enabled",
191+
_ => {
192+
find(id("grant-validator-license")).value.isEnabled shouldBe true
193+
},
194+
)
195+
196+
actAndCheck(
197+
"click the grant validator license button", {
198+
click on "grant-validator-license"
199+
200+
click on "grant-license-confirmation-dialog-accept-button"
201+
},
202+
)(
203+
"a new validator license row is added",
204+
_ => {
205+
checkValidatorLicenseRow(
206+
licenseRows.size.toLong,
207+
sv1Backend.getDsoInfo().svParty,
208+
newValidatorParty,
209+
)
210+
},
211+
)
212+
}
213+
}
214+
165215
"can view median amulet price and update desired amulet price by each SV" in { implicit env =>
166216
withFrontEnd("sv1") { implicit webDriver =>
167217
actAndCheck(

apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/SvMergeDuplicatedValidatorLicenseIntegrationTest.scala

Lines changed: 0 additions & 91 deletions
This file was deleted.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package org.lfdecentralizedtrust.splice.integration.tests
5+
6+
import com.digitalasset.canton.logging.SuppressionRule
7+
import com.digitalasset.canton.topology.PartyId
8+
import org.lfdecentralizedtrust.splice.codegen.java.splice.validatorlicense.ValidatorLicense
9+
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
10+
import org.lfdecentralizedtrust.splice.sv.automation.delegatebased.MergeValidatorLicenseContractsTrigger
11+
import org.lfdecentralizedtrust.splice.util.TriggerTestUtil
12+
import org.lfdecentralizedtrust.splice.util.TriggerTestUtil.{
13+
pauseAllDsoDelegateTriggers,
14+
resumeAllDsoDelegateTriggers,
15+
}
16+
import org.slf4j.event.Level
17+
import scala.jdk.OptionConverters.*
18+
19+
class SvValidatorLicenseIntegrationTest
20+
extends SvIntegrationTestBase
21+
with TriggerTestUtil
22+
with ExternallySignedPartyTestUtil {
23+
24+
override def environmentDefinition: EnvironmentDefinition =
25+
EnvironmentDefinition.simpleTopology1Sv(this.getClass.getSimpleName)
26+
27+
"grant validator license and verify merged licenses" in { implicit env =>
28+
val info = sv1Backend.getDsoInfo()
29+
val dsoParty = info.dsoParty
30+
val sv1Party = info.svParty
31+
32+
def getLicensesFromAliceValidator(p: PartyId) = {
33+
aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.acs
34+
.filterJava(ValidatorLicense.COMPANION)(
35+
dsoParty,
36+
c => c.data.validator == p.toProtoPrimitive,
37+
)
38+
}
39+
40+
// Allocate a new external party on aliceValidator
41+
val OnboardingResult(newParty, _, _) =
42+
onboardExternalParty(aliceValidatorBackend, Some("alice-test-party-2"))
43+
44+
// Test 1: Can allocate license to a new party on aliceValidator
45+
46+
val licenses = getLicensesFromAliceValidator(newParty)
47+
licenses should have length 0
48+
49+
actAndCheck(
50+
"Grant validator license to random new party on aliceValidator",
51+
sv1Backend.grantValidatorLicense(newParty),
52+
)(
53+
"ValidatorLicense granted to a random new party",
54+
_ => {
55+
val licenses = getLicensesFromAliceValidator(newParty)
56+
57+
licenses should have length 1
58+
licenses.head.data.validator shouldBe newParty.toProtoPrimitive
59+
licenses.head.data.sponsor shouldBe sv1Party.toProtoPrimitive
60+
},
61+
)
62+
63+
// Test 2: Verify that granting new license to existing validator merges all the
64+
// licenses to one, and does not affect its kind
65+
66+
val aliceValidatorParty = aliceValidatorBackend.getValidatorPartyId()
67+
val initialAliceLicenses = getLicensesFromAliceValidator(aliceValidatorParty)
68+
initialAliceLicenses should have length 1
69+
val initialKind = initialAliceLicenses.head.data.kind.toScala
70+
71+
// Pause the merge trigger to confirm multiple licenses exist
72+
pauseAllDsoDelegateTriggers[MergeValidatorLicenseContractsTrigger]
73+
74+
// Grant a license to Alice (creates NonOperatorLicense) with trigger paused
75+
actAndCheck(
76+
"Grant license to an onboarded validator",
77+
sv1Backend.grantValidatorLicense(aliceValidatorParty),
78+
)(
79+
"Two ValidatorLicenses exist for alice validator party",
80+
_ => {
81+
val licenses = getLicensesFromAliceValidator(aliceValidatorParty)
82+
83+
licenses should have length 2
84+
},
85+
)
86+
87+
// Resume the merge trigger and verify licenses got merged
88+
// The trigger can process both validator licenses in parallel so we might get multiple log messages.
89+
loggerFactory.assertLogsSeq(SuppressionRule.LevelAndAbove(Level.WARN))(
90+
{
91+
actAndCheck(
92+
"Resume merge trigger",
93+
resumeAllDsoDelegateTriggers[MergeValidatorLicenseContractsTrigger],
94+
)(
95+
"Licenses are merged while maintaining kind",
96+
_ => {
97+
val licenses = getLicensesFromAliceValidator(aliceValidatorParty)
98+
99+
licenses should have length 1
100+
licenses.head.data.validator shouldBe aliceValidatorParty.toProtoPrimitive
101+
licenses.head.data.kind.toScala shouldBe initialKind
102+
},
103+
)
104+
// Pause to make sure we don't get more log messages
105+
pauseAllDsoDelegateTriggers[MergeValidatorLicenseContractsTrigger]
106+
},
107+
forAll(_)(
108+
_.warningMessage should include(
109+
"has 2 Validator License contracts."
110+
)
111+
),
112+
)
113+
}
114+
}

apps/common/frontend-test-handlers/src/mocks/handlers/validator-licenses-handler.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { rest, RestHandler } from 'msw';
22

3-
import { ValidatorLicense } from '@daml.js/splice-amulet/lib/Splice/ValidatorLicense';
3+
import { LicenseKind, ValidatorLicense } from '@daml.js/splice-amulet/lib/Splice/ValidatorLicense';
44

55
export function validatorLicensesHandler(baseUrl: string): RestHandler {
66
return rest.get(`${baseUrl}/v0/admin/validator/licenses`, (req, res, ctx) => {
@@ -10,6 +10,26 @@ export function validatorLicensesHandler(baseUrl: string): RestHandler {
1010
const aTimestamp = '2024-09-26T16:15:36Z';
1111
const validatorLicenses = Array.from({ length: n }, (_, i) => {
1212
const id = (i + from).toString();
13+
const index = i + from;
14+
15+
// Vary weights: default (null), 1.5, 2.0, and 0.0
16+
let weight: string | null = null;
17+
if (index % 4 === 1) {
18+
weight = '1.5';
19+
} else if (index % 4 === 2) {
20+
weight = '2.0';
21+
} else if (index % 4 === 3) {
22+
weight = '0.0';
23+
}
24+
25+
// Vary kinds: null (default OperatorLicense), OperatorLicense, NonOperatorLicense
26+
let kind: LicenseKind | null = null;
27+
if (index % 3 === 1) {
28+
kind = 'OperatorLicense';
29+
} else if (index % 3 === 2) {
30+
kind = 'NonOperatorLicense';
31+
}
32+
1333
const validatorLicense: ValidatorLicense = {
1434
dso: 'dso',
1535
validator: `validator::${id}`,
@@ -21,8 +41,8 @@ export function validatorLicensesHandler(baseUrl: string): RestHandler {
2141
},
2242
metadata: { version: '1', lastUpdatedAt: aTimestamp, contactPoint: 'nowhere' },
2343
lastActiveAt: aTimestamp,
24-
weight: null,
25-
kind: null,
44+
weight,
45+
kind,
2646
};
2747
return {
2848
contract_id: id,

apps/common/frontend/src/components/ValidatorLicenses.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ import {
2121
TableHead,
2222
Typography,
2323
} from '@mui/material';
24+
import CheckIcon from '@mui/icons-material/Check';
2425
import TableBody from '@mui/material/TableBody';
2526
import TableCell from '@mui/material/TableCell';
2627
import TableRow from '@mui/material/TableRow';
2728

28-
import { ValidatorLicense } from '@daml.js/splice-amulet/lib/Splice/ValidatorLicense';
29+
import { LicenseKind, ValidatorLicense } from '@daml.js/splice-amulet/lib/Splice/ValidatorLicense';
2930
import { Party } from '@daml/types';
3031

3132
export interface ValidatorLicensesPage {
@@ -80,6 +81,8 @@ const ValidatorLicenses: React.FC<ValidatorLicensesProps> = ({
8081
<TableCell>Created at</TableCell>
8182
<TableCell>Validator</TableCell>
8283
<TableCell>Sponsor</TableCell>
84+
<TableCell>Weight</TableCell>
85+
<TableCell>Operator</TableCell>
8386
</TableRow>
8487
</TableHead>
8588
<TableBody>
@@ -91,6 +94,8 @@ const ValidatorLicenses: React.FC<ValidatorLicensesProps> = ({
9194
sponsor={license.payload.sponsor}
9295
createdAt={new Date(license.createdAt)}
9396
sv={dsoInfosQuery.data!.svPartyId}
97+
weight={license.payload.weight ?? undefined}
98+
kind={license.payload.kind ?? undefined}
9499
/>
95100
);
96101
})}
@@ -115,10 +120,22 @@ interface LicenseRowProps {
115120
sponsor: Party;
116121
createdAt: Date;
117122
sv: Party;
123+
weight?: string;
124+
kind?: LicenseKind;
118125
}
119126

120-
const LicenseRow: React.FC<LicenseRowProps> = ({ validator, sponsor, createdAt, sv }) => {
127+
const LicenseRow: React.FC<LicenseRowProps> = ({
128+
validator,
129+
sponsor,
130+
createdAt,
131+
sv,
132+
weight,
133+
kind,
134+
}) => {
121135
const sponsoredByThisSv = sponsor === sv;
136+
const isOperator = kind !== 'NonOperatorLicense';
137+
const displayWeight = weight || '';
138+
122139
return (
123140
<TableRow className="validator-licenses-table-row">
124141
<TableCell>
@@ -133,6 +150,8 @@ const LicenseRow: React.FC<LicenseRowProps> = ({ validator, sponsor, createdAt,
133150
{sponsoredByThisSv && <Chip label="THIS SV" color="primary" size="small" />}
134151
</Stack>
135152
</TableCell>
153+
<TableCell>{displayWeight}</TableCell>
154+
<TableCell>{isOperator ? <CheckIcon fontSize="medium" /> : ''}</TableCell>
136155
</TableRow>
137156
);
138157
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2+
-- SPDX-License-Identifier: Apache-2.0
3+
4+
-- For ValidatorLivenessActivityRecord weight field
5+
ALTER TABLE dso_acs_store ADD COLUMN validator_liveness_weight numeric;

0 commit comments

Comments
 (0)