From c5114aa43c8ab6b6ee6e299b218b4735955a6158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7=20Muntaner?= Date: Mon, 18 Sep 2023 19:16:59 +0200 Subject: [PATCH] GIX-1890: Check tx fee to disable disburse maturity button (#3329) # Motivation Check the fee to enable the disburse maturity button. # Changes * DisburseMaturityButton: Change prop to `disabledText`. * Add prop feeE8s to SnsDisburseMaturityButton * Add prop feeE8s to SnsAvailableMaturityItemAction. * Add prop feeE8s to SnsNeuronMaturitySection. * New sns neuron util hasEnoughMaturityToDisburse * Rename hasEnoughMaturityToStakeOrDisburse to hasEnoughMaturityToStake * Use new util `hasEnoughMaturityToDisburse` and new prop `disabledText` in SnsDisburseMaturityButton. * Change copy in "disburse_maturity_disabled_tooltip" i18n key. # Tests * Adapt tests to new props. * Adapt tests to `disabledText` prop name. * Change test case in SnsDisburseMaturityButton.spec to check agains a maturity less than fee. * New test case in SnsAvailableMaturityItemAction # Todos - [ ] Add entry to changelog (if necessary). Not worth an entry. Covered by disburse maturity entry. --- .../actions/DisburseMaturityButton.svelte | 11 +++-- .../SnsAvailableMaturityItemAction.svelte | 3 +- .../SnsNeuronMaturitySection.svelte | 3 +- .../actions/SnsDisburseMaturityButton.svelte | 18 ++++++-- .../actions/SnsStakeMaturityButton.svelte | 4 +- frontend/src/lib/i18n/en.json | 2 +- frontend/src/lib/pages/SnsNeuronDetail.svelte | 5 ++- frontend/src/lib/utils/sns-neuron.utils.ts | 15 ++++++- .../actions/DisburseMaturityButton.spec.ts | 10 ++--- .../SnsAvailableMaturityItemAction.spec.ts | 15 ++++++- .../SnsNeuronMaturitySection.spec.ts | 9 ++-- .../actions/SnsDisburseMaturityButton.spec.ts | 36 ++++++++++----- .../tests/lib/utils/sns-neuron.utils.spec.ts | 45 ++++++++++++++++--- .../DisburseMaturityButton.page-object.ts | 5 +++ 14 files changed, 135 insertions(+), 46 deletions(-) diff --git a/frontend/src/lib/components/neuron-detail/actions/DisburseMaturityButton.svelte b/frontend/src/lib/components/neuron-detail/actions/DisburseMaturityButton.svelte index 65371890071..cb36b317b61 100644 --- a/frontend/src/lib/components/neuron-detail/actions/DisburseMaturityButton.svelte +++ b/frontend/src/lib/components/neuron-detail/actions/DisburseMaturityButton.svelte @@ -2,20 +2,19 @@ import { i18n } from "$lib/stores/i18n"; import Tooltip from "$lib/components/ui/Tooltip.svelte"; import TestIdWrapper from "$lib/components/common/TestIdWrapper.svelte"; + import { isNullish } from "@dfinity/utils"; - export let enoughMaturity: boolean; + // If the button is disabled, this will be the tooltip text. + export let disabledText: string | undefined = undefined; - {#if enoughMaturity} + {#if isNullish(disabledText)} {:else} - + diff --git a/frontend/src/lib/components/sns-neuron-detail/SnsAvailableMaturityItemAction.svelte b/frontend/src/lib/components/sns-neuron-detail/SnsAvailableMaturityItemAction.svelte index 2c331c2c8a8..36a618cc958 100644 --- a/frontend/src/lib/components/sns-neuron-detail/SnsAvailableMaturityItemAction.svelte +++ b/frontend/src/lib/components/sns-neuron-detail/SnsAvailableMaturityItemAction.svelte @@ -14,6 +14,7 @@ import { ENABLE_DISBURSE_MATURITY } from "$lib/stores/feature-flags.store"; export let neuron: SnsNeuron; + export let feeE8s: bigint; let allowedToStakeMaturity: boolean; $: allowedToStakeMaturity = hasPermissionToStakeMaturity({ @@ -40,6 +41,6 @@ {/if} {#if allowedToDisburseMaturity && $ENABLE_DISBURSE_MATURITY} - + {/if} diff --git a/frontend/src/lib/components/sns-neuron-detail/SnsNeuronMaturitySection.svelte b/frontend/src/lib/components/sns-neuron-detail/SnsNeuronMaturitySection.svelte index 0667cc08edc..95d6f796fd9 100644 --- a/frontend/src/lib/components/sns-neuron-detail/SnsNeuronMaturitySection.svelte +++ b/frontend/src/lib/components/sns-neuron-detail/SnsNeuronMaturitySection.svelte @@ -8,6 +8,7 @@ import SnsViewActiveDisbursementsItemAction from "$lib/components/sns-neuron-detail/SnsViewActiveDisbursementsItemAction.svelte"; export let neuron: SnsNeuron; + export let feeE8s: bigint;
@@ -20,7 +21,7 @@

    - +
diff --git a/frontend/src/lib/components/sns-neuron-detail/actions/SnsDisburseMaturityButton.svelte b/frontend/src/lib/components/sns-neuron-detail/actions/SnsDisburseMaturityButton.svelte index 975d8dfa62c..f7714fb75f1 100644 --- a/frontend/src/lib/components/sns-neuron-detail/actions/SnsDisburseMaturityButton.svelte +++ b/frontend/src/lib/components/sns-neuron-detail/actions/SnsDisburseMaturityButton.svelte @@ -1,15 +1,27 @@ - + diff --git a/frontend/src/lib/components/sns-neuron-detail/actions/SnsStakeMaturityButton.svelte b/frontend/src/lib/components/sns-neuron-detail/actions/SnsStakeMaturityButton.svelte index ab5161c4ab2..ad8b1d8787f 100644 --- a/frontend/src/lib/components/sns-neuron-detail/actions/SnsStakeMaturityButton.svelte +++ b/frontend/src/lib/components/sns-neuron-detail/actions/SnsStakeMaturityButton.svelte @@ -1,13 +1,13 @@ diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index 5e6e3a5eb51..625dc19a57c 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -611,7 +611,7 @@ "spawn_neuron": "Spawn Neuron", "spawn": "Spawn", "stake_maturity_disabled_tooltip": "Currently, you do not have any maturity available to stake into this neuron.", - "disburse_maturity_disabled_tooltip": "Currently, you do not have any maturity available to disburse.", + "disburse_maturity_disabled_tooltip": "You do not have enough maturity to disburse. The minimum is: $fee.", "stake_maturity_tooltip": "Merge Maturity has been replaced by Stake Maturity. Learn more.", "start_dissolve_description": "This will cause your neuron to lose its age bonus.\nAre you sure you wish to continue?", "stop_dissolve_description": "Are you sure you want to stop the dissolve process?", diff --git a/frontend/src/lib/pages/SnsNeuronDetail.svelte b/frontend/src/lib/pages/SnsNeuronDetail.svelte index 4f10a9037ab..b4df6eb7292 100644 --- a/frontend/src/lib/pages/SnsNeuronDetail.svelte +++ b/frontend/src/lib/pages/SnsNeuronDetail.svelte @@ -179,7 +179,10 @@ {token} /> - + * Is the maturity of the neuron bigger than zero - i.e. has the neuron staked maturity? * @param {SnsNeuron} neuron */ -export const hasEnoughMaturityToStakeOrDisburse = ( +export const hasEnoughMaturityToStake = ( neuron: SnsNeuron | null | undefined ): boolean => (neuron?.maturity_e8s_equivalent ?? BigInt(0)) > BigInt(0); +/** + * Is the maturity of the neuron bigger than the transaction fee? + * @param {SnsNeuron} neuron + * @param {bigint} feeE8s + */ +export const hasEnoughMaturityToDisburse = ({ + neuron: { maturity_e8s_equivalent }, + feeE8s, +}: { + feeE8s: bigint; + neuron: SnsNeuron; +}): boolean => maturity_e8s_equivalent >= feeE8s; + /** * Does the neuron has staked maturity? * @param neuron diff --git a/frontend/src/tests/lib/components/neuron-detail/actions/DisburseMaturityButton.spec.ts b/frontend/src/tests/lib/components/neuron-detail/actions/DisburseMaturityButton.spec.ts index c4953bb32c4..2039334bbe5 100644 --- a/frontend/src/tests/lib/components/neuron-detail/actions/DisburseMaturityButton.spec.ts +++ b/frontend/src/tests/lib/components/neuron-detail/actions/DisburseMaturityButton.spec.ts @@ -8,10 +8,10 @@ import { JestPageObjectElement } from "$tests/page-objects/jest.page-object"; import { render } from "@testing-library/svelte"; describe("DisburseMaturityButton", () => { - const renderComponent = (enoughMaturity) => { + const renderComponent = (disabledText) => { const { container } = render(DisburseMaturityButton, { props: { - enoughMaturity, + disabledText, }, }); return DisburseMaturityButtonPo.under(new JestPageObjectElement(container)); @@ -22,19 +22,19 @@ describe("DisburseMaturityButton", () => { }); it("renders disburse maturity cta", async () => { - const po = renderComponent(true); + const po = renderComponent(undefined); expect(await po.isPresent()).toBe(true); }); it("should be enabled", async () => { - const po = renderComponent(true); + const po = renderComponent(undefined); expect(await po.isDisabled()).toBe(false); }); it("should be disabled", async () => { - const po = renderComponent(false); + const po = renderComponent("Disabled Text"); expect(await po.isDisabled()).toBe(true); }); diff --git a/frontend/src/tests/lib/components/sns-neuron-detail/SnsAvailableMaturityItemAction.spec.ts b/frontend/src/tests/lib/components/sns-neuron-detail/SnsAvailableMaturityItemAction.spec.ts index 792b8cb6148..cf410527622 100644 --- a/frontend/src/tests/lib/components/sns-neuron-detail/SnsAvailableMaturityItemAction.spec.ts +++ b/frontend/src/tests/lib/components/sns-neuron-detail/SnsAvailableMaturityItemAction.spec.ts @@ -29,10 +29,11 @@ describe("SnsAvailableMaturityItemAction", () => { maturity: 314000000n, permissions: [controllerPermissions], }); - const renderComponent = (neuron: SnsNeuron) => { + const renderComponent = (neuron: SnsNeuron, feeE8s = 10_000n) => { const { container } = render(SnsAvailableMaturityItemAction, { props: { neuron, + feeE8s, }, }); @@ -89,6 +90,18 @@ describe("SnsAvailableMaturityItemAction", () => { expect(await po.hasDisburseMaturityButton()).toBe(true); }); + it("should render disabled disburse maturity button when maturity is less than fee", async () => { + const fee = 100_000_000n; + const neuron = createMockSnsNeuron({ + id: [1], + maturity: fee - 1n, + permissions: [controllerPermissions], + }); + const po = renderComponent(neuron, fee); + + expect(await po.getDisburseMaturityButtonPo().isDisabled()).toBe(true); + }); + it("should not render stake maturity button if user has no disburse maturity permission", async () => { const neuron = createMockSnsNeuron({ id: [1], diff --git a/frontend/src/tests/lib/components/sns-neuron-detail/SnsNeuronMaturitySection.spec.ts b/frontend/src/tests/lib/components/sns-neuron-detail/SnsNeuronMaturitySection.spec.ts index 46f877838ae..b632b7fccf6 100644 --- a/frontend/src/tests/lib/components/sns-neuron-detail/SnsNeuronMaturitySection.spec.ts +++ b/frontend/src/tests/lib/components/sns-neuron-detail/SnsNeuronMaturitySection.spec.ts @@ -3,15 +3,14 @@ */ import SnsNeuronMaturitySection from "$lib/components/sns-neuron-detail/SnsNeuronMaturitySection.svelte"; -import { mockCanisterId } from "$tests/mocks/canisters.mock"; import { createMockSnsNeuron } from "$tests/mocks/sns-neurons.mock"; import { SnsNeuronMaturitySectionPo } from "$tests/page-objects/SnsNeuronMaturitySection.page-object"; import { JestPageObjectElement } from "$tests/page-objects/jest.page-object"; import type { SnsNeuron } from "@dfinity/sns"; import { render } from "@testing-library/svelte"; -import NeuronContextActionsTest from "./SnsNeuronContextTest.svelte"; describe("SnsNeuronMaturitySection", () => { + const feeE8s = 10_000n; const mockNeuron = createMockSnsNeuron({ id: [1], stakedMaturity: 100_000_000n, @@ -19,12 +18,10 @@ describe("SnsNeuronMaturitySection", () => { activeDisbursementsE8s: [200_000_000n], }); const renderComponent = (neuron: SnsNeuron) => { - const { container } = render(NeuronContextActionsTest, { + const { container } = render(SnsNeuronMaturitySection, { props: { neuron, - passPropNeuron: true, - rootCanisterId: mockCanisterId, - testComponent: SnsNeuronMaturitySection, + feeE8s, }, }); diff --git a/frontend/src/tests/lib/components/sns-neuron-detail/actions/SnsDisburseMaturityButton.spec.ts b/frontend/src/tests/lib/components/sns-neuron-detail/actions/SnsDisburseMaturityButton.spec.ts index 69ce370910d..6c45df18af1 100644 --- a/frontend/src/tests/lib/components/sns-neuron-detail/actions/SnsDisburseMaturityButton.spec.ts +++ b/frontend/src/tests/lib/components/sns-neuron-detail/actions/SnsDisburseMaturityButton.spec.ts @@ -6,13 +6,16 @@ import SnsDisburseMaturityButton from "$lib/components/sns-neuron-detail/actions import { mockSnsNeuron } from "$tests/mocks/sns-neurons.mock"; import { DisburseMaturityButtonPo } from "$tests/page-objects/DisburseMaturityButton.page-object"; import { JestPageObjectElement } from "$tests/page-objects/jest.page-object"; +import type { SnsNeuron } from "@dfinity/sns"; import { render } from "@testing-library/svelte"; describe("SnsDisburseMaturityButton", () => { - const renderComponent = (neuron) => { + const fee = 10_000n; + const renderComponent = (neuron: SnsNeuron, feeE8s: bigint) => { const { container } = render(SnsDisburseMaturityButton, { props: { neuron, + feeE8s, }, }); return DisburseMaturityButtonPo.under(new JestPageObjectElement(container)); @@ -23,23 +26,32 @@ describe("SnsDisburseMaturityButton", () => { }); it("should be enabled if enough maturity is available", async () => { - const po = renderComponent({ - ...mockSnsNeuron, - maturity_e8s_equivalent: 1n, - staked_maturity_e8s_equivalent: [], - }); + const po = renderComponent( + { + ...mockSnsNeuron, + maturity_e8s_equivalent: fee + 10n, + staked_maturity_e8s_equivalent: [], + }, + fee + ); expect(await po.isDisabled()).toBe(false); }); - it("should be disabled if no maturity to disburse", async () => { - const po = renderComponent({ - ...mockSnsNeuron, - maturity_e8s_equivalent: 0n, - staked_maturity_e8s_equivalent: [], - }); + it("should be disabled if less maturity than transaction fee", async () => { + const po = renderComponent( + { + ...mockSnsNeuron, + maturity_e8s_equivalent: fee - 1n, + staked_maturity_e8s_equivalent: [], + }, + fee + ); expect(await po.isDisabled()).toBe(true); + expect(await po.getTooltipText()).toBe( + "You do not have enough maturity to disburse. The minimum is: 0.0001." + ); }); it("should open disburse maturity modal", async () => { diff --git a/frontend/src/tests/lib/utils/sns-neuron.utils.spec.ts b/frontend/src/tests/lib/utils/sns-neuron.utils.spec.ts index 8423217741a..d5aa4c5f696 100644 --- a/frontend/src/tests/lib/utils/sns-neuron.utils.spec.ts +++ b/frontend/src/tests/lib/utils/sns-neuron.utils.spec.ts @@ -28,7 +28,8 @@ import { getSnsNeuronStake, getSnsNeuronState, getSnsNeuronVote, - hasEnoughMaturityToStakeOrDisburse, + hasEnoughMaturityToDisburse, + hasEnoughMaturityToStake, hasEnoughStakeToSplit, hasPermissionToDisburse, hasPermissionToDisburseMaturity, @@ -1361,11 +1362,16 @@ describe("sns-neuron utils", () => { describe("hasEnoughMaturityToStake", () => { it("should return true if staked maturity", () => { - const neuron = { + const neuron1 = { ...mockSnsNeuron, maturity_e8s_equivalent: BigInt(200000000), }; - expect(hasEnoughMaturityToStakeOrDisburse(neuron)).toBeTruthy(); + expect(hasEnoughMaturityToStake(neuron1)).toBe(true); + const neuron2 = { + ...mockSnsNeuron, + maturity_e8s_equivalent: 1n, + }; + expect(hasEnoughMaturityToStake(neuron2)).toBe(true); }); it("should return false if no staked maturity", () => { @@ -1374,12 +1380,39 @@ describe("sns-neuron utils", () => { maturity_e8s_equivalent: BigInt(0), }; - expect(hasEnoughMaturityToStakeOrDisburse(neuron)).toBe(false); + expect(hasEnoughMaturityToStake(neuron)).toBe(false); }); it("should return false when no neuron provided", () => { - expect(hasEnoughMaturityToStakeOrDisburse(null)).toBe(false); - expect(hasEnoughMaturityToStakeOrDisburse(undefined)).toBe(false); + expect(hasEnoughMaturityToStake(null)).toBe(false); + expect(hasEnoughMaturityToStake(undefined)).toBe(false); + }); + }); + + describe("hasEnoughMaturityToDisburse", () => { + const feeE8s = 10_000n; + it("should return true if maturity is more than fee", () => { + const neuron = { + ...mockSnsNeuron, + maturity_e8s_equivalent: feeE8s + 1n, + }; + expect(hasEnoughMaturityToDisburse({ neuron, feeE8s })).toBe(true); + }); + + it("should return false if maturity less than fee", () => { + const neuron = { + ...mockSnsNeuron, + maturity_e8s_equivalent: feeE8s - 1n, + }; + expect(hasEnoughMaturityToDisburse({ neuron, feeE8s })).toBe(false); + }); + + it("should return true if maturity is same as fee", () => { + const neuron = { + ...mockSnsNeuron, + maturity_e8s_equivalent: feeE8s, + }; + expect(hasEnoughMaturityToDisburse({ neuron, feeE8s })).toBe(true); }); }); diff --git a/frontend/src/tests/page-objects/DisburseMaturityButton.page-object.ts b/frontend/src/tests/page-objects/DisburseMaturityButton.page-object.ts index ec4d6608d34..54f498ac08c 100644 --- a/frontend/src/tests/page-objects/DisburseMaturityButton.page-object.ts +++ b/frontend/src/tests/page-objects/DisburseMaturityButton.page-object.ts @@ -1,6 +1,7 @@ import { ButtonPo } from "$tests/page-objects/Button.page-object"; import { BasePageObject } from "$tests/page-objects/base.page-object"; import type { PageObjectElement } from "$tests/types/page-object.types"; +import { TooltipPo } from "./Tooltip.page-object"; export class DisburseMaturityButtonPo extends BasePageObject { private static readonly TID = "disburse-maturity-button-component"; @@ -18,4 +19,8 @@ export class DisburseMaturityButtonPo extends BasePageObject { isDisabled(): Promise { return this.getButton().isDisabled(); } + + getTooltipText(): Promise { + return TooltipPo.under(this.root).getText(); + } }