-
Notifications
You must be signed in to change notification settings - Fork 115
Add UI support for fund manager blacklist and dev fund coupon mint delay #6944
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging-0.8.0
Are you sure you want to change the base?
Changes from all commits
1da9b10
4e48f1e
a518c88
860446c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -433,6 +433,101 @@ class DevelopmentFundCouponIntegrationTest | |||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| "Delaying the claiming of a development fund coupon until its mintAfter" in { implicit env => | ||||||||||||||||||||||
| onboardWalletUser(aliceValidatorWalletClient, aliceValidatorBackend) | ||||||||||||||||||||||
| val sv1UserId = sv1WalletClient.config.ledgerApiUser | ||||||||||||||||||||||
| val bobParty = onboardWalletUser(bobWalletClient, bobValidatorBackend) | ||||||||||||||||||||||
| val beneficiary = bobParty | ||||||||||||||||||||||
| val initialUnclaimedDevelopmentFundCouponAmount = BigDecimal(SpliceUtil.damlDecimal(1000)) | ||||||||||||||||||||||
| val developmentFundCouponAmount = BigDecimal(SpliceUtil.damlDecimal(40.0)) | ||||||||||||||||||||||
| val expiresAt = CantonTimestamp.now().plus(Duration.ofDays(1)) | ||||||||||||||||||||||
| val reason = "Bob has contributed to the Daml repo" | ||||||||||||||||||||||
| val mintingDelay = Duration.ofSeconds(30) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| val bobUserName = bobWalletClient.config.ledgerApiUser | ||||||||||||||||||||||
| val bobMergeAmuletsTrigger = | ||||||||||||||||||||||
| bobValidatorBackend | ||||||||||||||||||||||
| .userWalletAutomation(bobUserName) | ||||||||||||||||||||||
| .futureValue | ||||||||||||||||||||||
| .trigger[CollectRewardsAndMergeAmuletsTrigger] | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| archiveExistingUnclaimedDevelopmentFundCoupons() | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this looks fishy. We usually do test isolation by relying on fresh parties being allocated for new test instances. So if you scope your queries wrt coupons being created to the test-instance-specific party, then you have proper isolation. |
||||||||||||||||||||||
| actAndCheck( | ||||||||||||||||||||||
| "Mint one unclaimed development fund coupon", { | ||||||||||||||||||||||
| createUnclaimedDevelopmentFundCoupon( | ||||||||||||||||||||||
| sv1ValidatorBackend.participantClientWithAdminToken, | ||||||||||||||||||||||
| sv1UserId, | ||||||||||||||||||||||
| initialUnclaimedDevelopmentFundCouponAmount, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| )( | ||||||||||||||||||||||
| "The unclaimed development fund coupon is created", | ||||||||||||||||||||||
| _ => { | ||||||||||||||||||||||
| getUnclaimedDevelopmentFundCouponTotal( | ||||||||||||||||||||||
| sv1ValidatorBackend | ||||||||||||||||||||||
| ) shouldBe initialUnclaimedDevelopmentFundCouponAmount | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| val bobBalanceBefore = bobWalletClient.balance().unlockedQty | ||||||||||||||||||||||
| val (mintAfter, _) = setTriggersWithin( | ||||||||||||||||||||||
| triggersToPauseAtStart = Seq(bobMergeAmuletsTrigger) | ||||||||||||||||||||||
| ) { | ||||||||||||||||||||||
| actAndCheck( | ||||||||||||||||||||||
| "Allocate one development fund coupon that is not mintable yet", { | ||||||||||||||||||||||
| val mintAfter = CantonTimestamp.now().plus(mintingDelay) | ||||||||||||||||||||||
| aliceValidatorWalletClient.allocateDevelopmentFundCoupon( | ||||||||||||||||||||||
| beneficiary, | ||||||||||||||||||||||
| developmentFundCouponAmount, | ||||||||||||||||||||||
| expiresAt, | ||||||||||||||||||||||
| reason, | ||||||||||||||||||||||
| Some(mintAfter), | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| mintAfter | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| )( | ||||||||||||||||||||||
| "The coupon is created and carries the requested mintAfter", | ||||||||||||||||||||||
| allocatedMintAfter => { | ||||||||||||||||||||||
| val coupons = bobWalletClient.listActiveDevelopmentFundCoupons() | ||||||||||||||||||||||
| coupons should have size 1 withClue "bob coupons" | ||||||||||||||||||||||
| coupons.head.payload.mintAfter shouldBe java.util.Optional.of( | ||||||||||||||||||||||
| allocatedMintAfter.toInstant | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| clue("The coupon is left alone while its mintAfter is in the future") { | ||||||||||||||||||||||
| always(durationOfSuccess = 10.seconds) { | ||||||||||||||||||||||
| bobWalletClient | ||||||||||||||||||||||
| .listActiveDevelopmentFundCoupons() should have size 1 withClue "bob coupons before mintAfter" | ||||||||||||||||||||||
| bobWalletClient.balance().unlockedQty shouldBe bobBalanceBefore | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| CantonTimestamp | ||||||||||||||||||||||
| .now() | ||||||||||||||||||||||
| .isBefore(mintAfter) shouldBe true withClue "still before mintAfter" | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+500
to
+509
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need to test this, as that's already tested at the Daml level.
Suggested change
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| clue("The coupon is collected once its mintAfter has passed") { | ||||||||||||||||||||||
| eventually(60.seconds) { | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is very expensive! Let's avoid it.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the above suggestion, this would be 30s, as the |
||||||||||||||||||||||
| bobWalletClient | ||||||||||||||||||||||
| .listActiveDevelopmentFundCoupons() shouldBe empty withClue "bob coupons after mintAfter" | ||||||||||||||||||||||
| bobWalletClient.balance().unlockedQty shouldBe | ||||||||||||||||||||||
| (bobBalanceBefore + developmentFundCouponAmount) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| clue("The collected coupon is listed in listDevelopmentFundCouponHistory as claimed") { | ||||||||||||||||||||||
| eventually() { | ||||||||||||||||||||||
| assertListDevelopmentFundCouponHistoryStatuses( | ||||||||||||||||||||||
| bobWalletClient, | ||||||||||||||||||||||
| beneficiary, | ||||||||||||||||||||||
| Seq(httpDef.ArchivedDevelopmentFundCoupon.Status.Claimed -> None), | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| "Expiring a development fund coupon" in { implicit env => | ||||||||||||||||||||||
| val sv1UserId = sv1WalletClient.config.ledgerApiUser | ||||||||||||||||||||||
| onboardWalletUser(aliceValidatorWalletClient, aliceValidatorBackend) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -659,6 +659,119 @@ class WalletMintingDelegationTimeBasedIntegrationTest | |
| } | ||
| } | ||
|
|
||
| "not collect a development fund coupon before its mintAfter" in { implicit env => | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For the same reason as explained above, I don't think we need an additional test, which increases both CI and maintenance cost. We just want to check that the coupons are ignored when their time is not due, and collected afterwards. I'd suggest to restructure the existing test to do so as follows:
|
||
| val aliceParty = onboardWalletUser(aliceWalletClient, aliceValidatorBackend) | ||
| aliceWalletClient.tap(100.0) | ||
| aliceValidatorWalletClient.tap(100.0) | ||
|
|
||
| val beneficiaryParty = | ||
| onboardExternalParty(aliceValidatorBackend, Some("delayed_coupon_beneficiary")) | ||
| createAndAcceptExternalPartySetupProposal(aliceValidatorBackend, beneficiaryParty) | ||
|
|
||
| val delegationExpiresAt = env.environment.clock.now.plus(Duration.ofDays(30)).toInstant | ||
| val (_, proposalContractId) = actAndCheck( | ||
| "Create minting delegation proposal", | ||
| createMintingDelegationProposal(beneficiaryParty, aliceParty, delegationExpiresAt), | ||
| )( | ||
| "Proposal is visible", | ||
| _ => { | ||
| val proposals = aliceWalletClient.listMintingDelegationProposals() | ||
| proposals.proposals should have size 1 withClue "proposals" | ||
| proposals.proposals.head.contract.contractId | ||
| }, | ||
| ) | ||
|
|
||
| actAndCheck( | ||
| "Alice accepts the proposal", | ||
| aliceWalletClient.acceptMintingDelegationProposal(proposalContractId), | ||
| )( | ||
| "Delegation is created", | ||
| _ => { | ||
| val delegations = aliceWalletClient.listMintingDelegations() | ||
| delegations.delegations should have size 1 withClue "delegations" | ||
| }, | ||
| ) | ||
|
|
||
| val externalPartyWallet = eventually() { | ||
| aliceValidatorBackend.appState.walletManager | ||
| .valueOrFail("WalletManager is expected to be defined") | ||
| .externalPartyWalletManager | ||
| .lookupExternalPartyWallet(beneficiaryParty.party) | ||
| .valueOrFail( | ||
| s"Expected ${beneficiaryParty.party} to have an external party wallet" | ||
| ) | ||
| } | ||
|
|
||
| def getBalance(): BigDecimal = BigDecimal( | ||
| aliceValidatorBackend | ||
| .getExternalPartyBalance(beneficiaryParty.party) | ||
| .totalUnlockedCoin | ||
| ) | ||
|
|
||
| advanceRoundsToNextRoundOpening | ||
| advanceRoundsToNextRoundOpening | ||
|
|
||
| val balanceBefore = getBalance() | ||
| val developmentFundAmount = BigDecimal(300.0) | ||
| // Short enough that advancing past it does not disturb round automation. | ||
| val mintDelay = Duration.ofMinutes(10) | ||
|
|
||
| val mintAfter = env.environment.clock.now.plus(mintDelay).toInstant | ||
| val couponExpiresAt = env.environment.clock.now.plus(Duration.ofDays(30)).toInstant | ||
|
|
||
| val validatorRewardTrigger = collectRewardsAndMergeAmuletsTrigger( | ||
| aliceValidatorBackend, | ||
| aliceValidatorWalletClient.config.ledgerApiUser, | ||
| ) | ||
|
|
||
| setTriggersWithin(triggersToPauseAtStart = Seq(validatorRewardTrigger)) { | ||
| val externalPartyMintingDelegationTrigger = mintingDelegationCollectRewardsTrigger( | ||
| aliceValidatorBackend, | ||
| beneficiaryParty.party, | ||
| ) | ||
|
|
||
| setTriggersWithin(triggersToPauseAtStart = Seq(externalPartyMintingDelegationTrigger)) { | ||
| sv1Backend.participantClientWithAdminToken.ledger_api_extensions.commands | ||
| .submitWithResult( | ||
| userId = sv1Backend.config.ledgerApiUser, | ||
| actAs = Seq(dsoParty), | ||
| readAs = Seq.empty, | ||
| update = new DevelopmentFundCoupon( | ||
| dsoParty.toProtoPrimitive, | ||
| beneficiaryParty.party.toProtoPrimitive, | ||
| dsoParty.toProtoPrimitive, | ||
| developmentFundAmount.bigDecimal, | ||
| couponExpiresAt, | ||
| "delayed development fund coupon", | ||
| java.util.Optional.of(mintAfter), | ||
| ).create, | ||
| ) | ||
| } | ||
|
|
||
| clue("Coupon is left alone while mintAfter is in the future") { | ||
| (1 to 3).foreach(_ => advanceTime(Duration.ofMinutes(1))) | ||
| externalPartyWallet.store | ||
| .listDevelopmentFundCoupons() | ||
| .futureValue should have size 1 withClue "DevelopmentFundCoupon before mintAfter" | ||
| getBalance() shouldBe balanceBefore | ||
| } | ||
|
|
||
| actAndCheck( | ||
| "Advance past mintAfter", | ||
| advanceTime(mintDelay.plus(Duration.ofHours(1))), | ||
| )( | ||
| "Coupon is collected", | ||
| _ => { | ||
| advanceTime(Duration.ofSeconds(1)) | ||
| externalPartyWallet.store | ||
| .listDevelopmentFundCoupons() | ||
| .futureValue shouldBe empty withClue "DevelopmentFundCoupon after mintAfter" | ||
| getBalance() shouldBe balanceBefore + developmentFundAmount | ||
| }, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| "assign and mint unassigned V2 coupons when sharing is configured" in { implicit env => | ||
| val aliceParty = onboardWalletUser(aliceWalletClient, aliceValidatorBackend) | ||
| aliceWalletClient.tap(100.0) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -346,6 +346,50 @@ describe('buildAmuletRulesConfigFromChanges', () => { | |
| ]); | ||
| }); | ||
|
|
||
| test('should round-trip the development fund blacklist and minting delay', () => { | ||
| const changes: ConfigChange[] = [ | ||
| { | ||
| fieldName: 'developmentFundManagerBlacklist', | ||
| label: 'Development Fund Manager Blacklist', | ||
| currentValue: 'alice::122', | ||
| newValue: 'alice::122, bob::122', | ||
| }, | ||
| { | ||
| fieldName: 'minDevelopmentFundMintingDelay', | ||
| label: 'Min Development Fund Minting Delay', | ||
| currentValue: '', | ||
| newValue: '604800000000', | ||
| }, | ||
| ]; | ||
|
|
||
| const result = buildAmuletRulesConfigFromChanges(changes); | ||
|
|
||
| expect(result.developmentFundManagerBlacklist).toEqual(['alice::122', 'bob::122']); | ||
| expect(result.minDevelopmentFundMintingDelay).toEqual({ microseconds: '604800000000' }); | ||
| }); | ||
|
|
||
| test('should map an emptied development fund blacklist to an empty list and the delay to null', () => { | ||
| const changes: ConfigChange[] = [ | ||
| { | ||
| fieldName: 'developmentFundManagerBlacklist', | ||
| label: 'Development Fund Manager Blacklist', | ||
| currentValue: 'alice::122', | ||
| newValue: ' , ', | ||
| }, | ||
| { | ||
| fieldName: 'minDevelopmentFundMintingDelay', | ||
| label: 'Min Development Fund Minting Delay', | ||
| currentValue: '604800000000', | ||
| newValue: '', | ||
| }, | ||
| ]; | ||
|
|
||
| const result = buildAmuletRulesConfigFromChanges(changes); | ||
|
|
||
| expect(result.developmentFundManagerBlacklist).toEqual([]); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wdyt about setting it to |
||
| expect(result.minDevelopmentFundMintingDelay).toBeNull(); | ||
| }); | ||
|
|
||
| test('should handle issuance curve future values', () => { | ||
| const changes: ConfigChange[] = [ | ||
| { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When testing aspects related to timing we usually use a TimeBased integration test, which allowS changing the clock using
advanceTimewithout busy waiting.However, all the logic wrt minting delays is already tested at the Daml level. So what we need to test here is that a coupon with a minting delay gets eventually minted without raising unexpected errors.
I'd suggest we set a delay of
10s, which is long enough so it would result in a retry if the minting would be attempted, but short enough so its CI cost is not egregious. I'd then add 10s extra wait time also to theeventuallythat checks whether the coupon gets minted.For bonus points, I'd change the minting code once to not respect the mint after, and check what kind of errors are raised in the logs (I expect retries); and then check again with the proper filtering that these are gone.