Skip to content

Commit fd2600b

Browse files
Tim Emioladfordivam
authored andcommitted
Wallet UI support for minting delegations management
PR reviewed on #3601 Signed-off-by: Tim Emiola <adetokunbo@emio.la>
1 parent 2054fce commit fd2600b

File tree

16 files changed

+1349
-29
lines changed

16 files changed

+1349
-29
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,16 @@ trait FrontendTestCommon extends TestCommon with WebBrowser with CustomMatchers
684684
clickOn(query)
685685
}
686686

687+
protected def clickByCssSelector(selector: String)(implicit
688+
webDriver: WebDriver
689+
): Unit = {
690+
val query = cssSelector(selector)
691+
waitForCondition(query) {
692+
ExpectedConditions.elementToBeClickable(_)
693+
}
694+
eventuallyClickOn(query)
695+
}
696+
687697
protected def eventuallyFind(query: Query)(implicit driver: WebDriver) = {
688698
clue(s"Waiting for $query to be found") {
689699
waitForCondition(query) {

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

Lines changed: 277 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.lfdecentralizedtrust.splice.integration.tests
22

3+
import org.lfdecentralizedtrust.splice.codegen.java.splice.wallet.mintingdelegation as mintingDelegationCodegen
34
import org.lfdecentralizedtrust.splice.integration.EnvironmentDefinition
45
import org.lfdecentralizedtrust.splice.integration.tests.SpliceTests.SpliceTestConsoleEnvironment
56
import org.lfdecentralizedtrust.splice.util.{
@@ -8,12 +9,18 @@ import org.lfdecentralizedtrust.splice.util.{
89
WalletFrontendTestUtil,
910
WalletTestUtil,
1011
}
12+
import com.digitalasset.canton.topology.PartyId
13+
import org.openqa.selenium.WebDriver
14+
15+
import java.time.Duration
16+
import scala.jdk.CollectionConverters.*
1117

1218
class WalletFrontendIntegrationTest
1319
extends FrontendIntegrationTestWithSharedEnvironment("alice")
1420
with WalletTestUtil
1521
with WalletFrontendTestUtil
16-
with FrontendLoginUtil {
22+
with FrontendLoginUtil
23+
with ExternallySignedPartyTestUtil {
1724

1825
val amuletPrice = 2
1926
override def walletAmuletPrice = SpliceUtil.damlDecimal(amuletPrice.toDouble)
@@ -200,6 +207,245 @@ class WalletFrontendIntegrationTest
200207

201208
}
202209

210+
"with delegations and their proposals" should {
211+
212+
"allow them to be accepted, rejected or withdrawn as appropriate" in { implicit env =>
213+
def checkRowCounts(proposalCount: Long, activeCount: Long)(implicit
214+
webDriver: WebDriver
215+
): Unit = {
216+
val proposalRows = findAll(className("proposal-row")).toSeq
217+
proposalRows should have size proposalCount
218+
val delegationRows = findAll(className("delegation-row")).toSeq
219+
delegationRows should have size activeCount
220+
}
221+
222+
// 1. Setup
223+
val aliceDamlUser = aliceWalletClient.config.ledgerApiUser
224+
onboardWalletUser(aliceWalletClient, aliceValidatorBackend)
225+
val aliceParty =
226+
PartyId.tryFromProtoPrimitive(aliceWalletClient.userStatus().party)
227+
228+
// Tap to fund the validator wallet for external party setup
229+
// (external party operations go through aliceValidatorBackend)
230+
aliceValidatorWalletClient.tap(100.0)
231+
232+
// Onboard three external parties as beneficiaries
233+
val beneficiary1Onboarding =
234+
onboardExternalParty(aliceValidatorBackend, Some("beneficiary1"))
235+
createAndAcceptExternalPartySetupProposal(aliceValidatorBackend, beneficiary1Onboarding)
236+
237+
val beneficiary2Onboarding =
238+
onboardExternalParty(aliceValidatorBackend, Some("beneficiary2"))
239+
createAndAcceptExternalPartySetupProposal(aliceValidatorBackend, beneficiary2Onboarding)
240+
241+
val beneficiary3Onboarding =
242+
onboardExternalParty(aliceValidatorBackend, Some("beneficiary3"))
243+
createAndAcceptExternalPartySetupProposal(aliceValidatorBackend, beneficiary3Onboarding)
244+
245+
// 2. Verify empty initial state via API
246+
clue("Check that no minting delegation proposals exist initially") {
247+
aliceWalletClient.listMintingDelegationProposals().proposals shouldBe empty
248+
}
249+
clue("Check that no minting delegations exist initially") {
250+
aliceWalletClient.listMintingDelegations().delegations shouldBe empty
251+
}
252+
253+
// 3. Create three proposals, one from each beneficiary
254+
val envNow = env.environment.clock.now
255+
val expiresAt = envNow.plus(Duration.ofDays(30)).toInstant
256+
val expiresDayAfter = envNow.plus(Duration.ofDays(31)).toInstant
257+
actAndCheck(
258+
"Each beneficiary creates a minting delegation proposal", {
259+
createMintingDelegationProposal(beneficiary1Onboarding, aliceParty, expiresAt)
260+
createMintingDelegationProposal(beneficiary2Onboarding, aliceParty, expiresAt)
261+
createMintingDelegationProposal(beneficiary3Onboarding, aliceParty, expiresDayAfter)
262+
},
263+
)(
264+
"and they are successfully created",
265+
_ => {
266+
aliceWalletClient
267+
.listMintingDelegationProposals()
268+
.proposals should have size 3
269+
},
270+
)
271+
272+
// 4. Test via Selenium UI (using Alice's wallet frontend)
273+
withFrontEnd("alice") { implicit webDriver =>
274+
actAndCheck(
275+
"Alice browses to the wallet", {
276+
browseToAliceWallet(aliceDamlUser)
277+
},
278+
)(
279+
"Alice sees the Delegations tab",
280+
_ => {
281+
waitForQuery(id("navlink-delegations"))
282+
},
283+
)
284+
285+
actAndCheck(
286+
"Alice clicks on Delegations tab", {
287+
eventuallyClickOn(id("navlink-delegations"))
288+
},
289+
)(
290+
"Alice sees the Proposed table with 3 proposals and empty Delegations table",
291+
_ => {
292+
find(id("proposals-label")).valueOrFail("Proposed heading not found!")
293+
val proposalRows = findAll(className("proposal-row")).toSeq
294+
proposalRows should have size 3
295+
296+
proposalRows.foreach { row =>
297+
row
298+
.findChildElement(className("proposal-accept"))
299+
.valueOrFail("Accept button not found in proposal row!")
300+
row
301+
.findChildElement(className("proposal-reject"))
302+
.valueOrFail("Reject button not found in proposal row!")
303+
}
304+
305+
find(id("delegations-label")).valueOrFail("Delegations heading not found!")
306+
find(id("no-delegations-message")).valueOrFail("No delegations message not found!")
307+
},
308+
)
309+
310+
// 5. Accept first proposal via UI
311+
actAndCheck(
312+
"Alice clicks Accept on the first proposal and confirms", {
313+
clickByCssSelector(".proposal-row .proposal-accept")
314+
eventuallyClickOn(id("accept-proposal-confirmation-dialog-accept-button"))
315+
},
316+
)(
317+
"2 proposals remain, 1 delegation created",
318+
_ => {
319+
eventually() {
320+
checkRowCounts(2, 1)
321+
}
322+
},
323+
)
324+
325+
// 6. Accept second proposal via UI
326+
actAndCheck(
327+
"Alice clicks Accept on the second proposal and confirms", {
328+
clickByCssSelector(".proposal-row .proposal-accept")
329+
eventuallyClickOn(id("accept-proposal-confirmation-dialog-accept-button"))
330+
},
331+
)(
332+
"1 proposal remains, 2 delegations exist",
333+
_ => {
334+
eventually() {
335+
checkRowCounts(1, 2)
336+
}
337+
},
338+
)
339+
340+
// 7. Withdraw one delegation via UI
341+
actAndCheck(
342+
"Alice clicks Withdraw on the first delegation and confirms", {
343+
clickByCssSelector(".delegation-row .delegation-withdraw")
344+
eventuallyClickOn(id("withdraw-delegation-confirmation-dialog-accept-button"))
345+
},
346+
)(
347+
"1 proposal remains, 1 delegation remains",
348+
_ => {
349+
eventually() {
350+
checkRowCounts(1, 1)
351+
}
352+
},
353+
)
354+
355+
// 8. Reject the final proposal via UI
356+
actAndCheck(
357+
"Alice clicks Reject on the final proposal and confirms", {
358+
clickByCssSelector(".proposal-row .proposal-reject")
359+
eventuallyClickOn(id("reject-proposal-confirmation-dialog-accept-button"))
360+
},
361+
)(
362+
"No proposals remain, 1 delegation remains",
363+
_ => {
364+
eventually() {
365+
find(id("no-proposals-message")).valueOrFail("No proposals message not found!")
366+
checkRowCounts(0, 1)
367+
}
368+
},
369+
)
370+
371+
// 9. Create two proposals, from beneficiaries that have not completedly onboarding
372+
val beneficiary4Incomplete =
373+
onboardExternalParty(aliceValidatorBackend, Some("beneficiary4"))
374+
val beneficiary5Incomplete =
375+
onboardExternalParty(aliceValidatorBackend, Some("beneficiary5"))
376+
actAndCheck(
377+
"Each beneficiary creates a minting delegation proposal", {
378+
createMintingDelegationProposal(beneficiary4Incomplete, aliceParty, expiresDayAfter)
379+
createMintingDelegationProposal(beneficiary5Incomplete, aliceParty, expiresDayAfter)
380+
webDriver.navigate().refresh()
381+
},
382+
)(
383+
"2 new proposals appear, 1 delegation remains",
384+
_ => {
385+
eventually() {
386+
checkRowCounts(2, 1)
387+
}
388+
},
389+
)
390+
391+
actAndCheck(
392+
"Accept a non-onboarded proposal via api, refresh the UI", {
393+
val proposals = aliceWalletClient.listMintingDelegationProposals()
394+
val cid = proposals.proposals.head.contract.contractId
395+
aliceWalletClient.acceptMintingDelegationProposal(cid)
396+
webDriver.navigate().refresh()
397+
},
398+
)(
399+
"1 proposal remain, 2 delegations are visible",
400+
_ => {
401+
eventually() {
402+
checkRowCounts(1, 2)
403+
}
404+
},
405+
)
406+
407+
// 10. Add another proposal, refresh the UI and confirm that it appears
408+
actAndCheck(
409+
"Beneficiary 2 creates new minting proposal and UI refreshes", {
410+
createLimitedMintingDelegationProposal(
411+
beneficiary2Onboarding,
412+
aliceParty,
413+
expiresAt,
414+
18,
415+
)
416+
webDriver.navigate().refresh()
417+
},
418+
)(
419+
"1 new proposal appears making 2, 2 delegations remain",
420+
_ => {
421+
eventually() {
422+
aliceWalletClient
423+
.listMintingDelegationProposals()
424+
.proposals should have size 2
425+
checkRowCounts(2, 2)
426+
}
427+
},
428+
)
429+
430+
// 11. Accept proposal that causes automatic withdraw of existing delegation for beneficary 2
431+
actAndCheck(
432+
"Alice clicks Accept on new proposal and confirms", {
433+
clickByCssSelector(".proposal-row .proposal-accept")
434+
eventuallyClickOn(id("accept-proposal-confirmation-dialog-accept-button"))
435+
},
436+
)(
437+
"1 proposal and 2 delegations remain",
438+
_ => {
439+
eventually() {
440+
checkRowCounts(1, 2)
441+
}
442+
},
443+
)
444+
}
445+
}
446+
447+
}
448+
203449
"show logged in ANS name" in { implicit env =>
204450
// Create directory entry for alice
205451
val aliceDamlUser = aliceWalletClient.config.ledgerApiUser
@@ -239,6 +485,36 @@ class WalletFrontendIntegrationTest
239485
)
240486
}
241487
}
488+
}
242489

490+
private def createMintingDelegationProposal(
491+
beneficiaryOnboarding: OnboardingResult,
492+
delegate: PartyId,
493+
expiresAt: java.time.Instant,
494+
)(implicit env: SpliceTestConsoleEnvironment): Unit =
495+
createLimitedMintingDelegationProposal(beneficiaryOnboarding, delegate, expiresAt, 10)
496+
497+
private def createLimitedMintingDelegationProposal(
498+
beneficiaryOnboarding: OnboardingResult,
499+
delegate: PartyId,
500+
expiresAt: java.time.Instant,
501+
mergeLimit: Int,
502+
)(implicit env: SpliceTestConsoleEnvironment): Unit = {
503+
val beneficiary = beneficiaryOnboarding.party
504+
val proposal = new mintingDelegationCodegen.MintingDelegationProposal(
505+
new mintingDelegationCodegen.MintingDelegation(
506+
beneficiary.toProtoPrimitive,
507+
delegate.toProtoPrimitive,
508+
dsoParty.toProtoPrimitive,
509+
expiresAt,
510+
mergeLimit,
511+
)
512+
)
513+
// Use externally signed submission for the external party
514+
aliceValidatorBackend.participantClientWithAdminToken.ledger_api_extensions.commands
515+
.submitJavaExternalOrLocal(
516+
actingParty = beneficiaryOnboarding.richPartyId,
517+
commands = proposal.create.commands.asScala.toSeq,
518+
)
243519
}
244520
}

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,12 @@ class WalletMintingDelegationTimeBasedIntegrationTest
113113
}
114114

115115
// Test 1
116-
clue("Test beneficiaryOnboarded status") {
116+
clue("Test beneficiaryHosted status") {
117117
val (_, proposalBeforeOnboardingCid) = actAndCheck(
118-
"Create minting delegation proposal before beneficiary is onboarded",
118+
"Create minting delegation proposal before beneficiary is hosted",
119119
createMintingDelegationProposal(beneficiaryParty, validatorParty, expiresAt),
120120
)(
121-
"Proposal is visible with beneficiaryOnboarded = false",
121+
"Proposal is visible with beneficiaryHosted = false",
122122
_ => {
123123
val proposals = aliceValidatorWalletClient.listMintingDelegationProposals()
124124
proposals.proposals should have size 2
@@ -130,17 +130,17 @@ class WalletMintingDelegationTimeBasedIntegrationTest
130130
.contains(beneficiaryParty.party.toProtoPrimitive)
131131
)
132132
.value
133-
beneficiaryProposal.beneficiaryOnboarded shouldBe false
133+
beneficiaryProposal.beneficiaryHosted shouldBe false
134134
beneficiaryProposal.contract.contractId
135135
},
136136
)
137137

138-
// Accept the proposal before onboarding and verify beneficiaryOnboarded = false in delegations
138+
// Accept the proposal before hosting and verify beneficiaryHosted = false in delegations
139139
actAndCheck(
140-
"Accept proposal before beneficiary is onboarded",
140+
"Accept proposal before beneficiary is hosted",
141141
aliceValidatorWalletClient.acceptMintingDelegationProposal(proposalBeforeOnboardingCid),
142142
)(
143-
"Delegation is visible with beneficiaryOnboarded = false",
143+
"Delegation is visible with beneficiaryHosted = false",
144144
_ => {
145145
val delegations = aliceValidatorWalletClient.listMintingDelegations()
146146
delegations.delegations should have size 2
@@ -151,15 +151,15 @@ class WalletMintingDelegationTimeBasedIntegrationTest
151151
.contains(beneficiaryParty.party.toProtoPrimitive)
152152
)
153153
.value
154-
beneficiaryDelegation.beneficiaryOnboarded shouldBe false
154+
beneficiaryDelegation.beneficiaryHosted shouldBe false
155155
},
156156
)
157157
}
158158

159159
// Onboard beneficiary
160160
createAndAcceptExternalPartySetupProposal(aliceValidatorBackend, beneficiaryParty)
161161

162-
clue("After onboarding, beneficiaryOnboarded should be true in delegations") {
162+
clue("After hosting, beneficiaryHosted should be true in delegations") {
163163
val delegations = aliceValidatorWalletClient.listMintingDelegations()
164164
val beneficiaryDelegation = delegations.delegations
165165
.find(
@@ -168,7 +168,7 @@ class WalletMintingDelegationTimeBasedIntegrationTest
168168
.contains(beneficiaryParty.party.toProtoPrimitive)
169169
)
170170
.value
171-
beneficiaryDelegation.beneficiaryOnboarded shouldBe true
171+
beneficiaryDelegation.beneficiaryHosted shouldBe true
172172
}
173173

174174
// Test 2: Creates a proposal and test reject

0 commit comments

Comments
 (0)