11package org .lfdecentralizedtrust .splice .integration .tests
22
3+ import org .lfdecentralizedtrust .splice .codegen .java .splice .wallet .mintingdelegation as mintingDelegationCodegen
34import org .lfdecentralizedtrust .splice .integration .EnvironmentDefinition
45import org .lfdecentralizedtrust .splice .integration .tests .SpliceTests .SpliceTestConsoleEnvironment
56import 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
1218class 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}
0 commit comments