Skip to content

Commit

Permalink
GIX-1890: Check tx fee to disable disburse maturity button (#3329)
Browse files Browse the repository at this point in the history
# 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.
  • Loading branch information
lmuntaner authored Sep 18, 2023
1 parent 63e81ea commit c5114aa
Show file tree
Hide file tree
Showing 14 changed files with 135 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
</script>

<TestIdWrapper testId="disburse-maturity-button-component">
{#if enoughMaturity}
{#if isNullish(disabledText)}
<button class="secondary" on:click
>{$i18n.neuron_detail.disburse_maturity}</button
>
{:else}
<Tooltip
id="stake-maturity-tooltip"
text={$i18n.neuron_detail.disburse_maturity_disabled_tooltip}
>
<Tooltip id="stake-maturity-tooltip" text={disabledText}>
<button class="secondary" disabled
>{$i18n.neuron_detail.disburse_maturity}</button
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -40,6 +41,6 @@
{/if}

{#if allowedToDisburseMaturity && $ENABLE_DISBURSE_MATURITY}
<SnsDisburseMaturityButton {neuron} />
<SnsDisburseMaturityButton {neuron} {feeE8s} />
{/if}
</CommonItemAction>
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import SnsViewActiveDisbursementsItemAction from "$lib/components/sns-neuron-detail/SnsViewActiveDisbursementsItemAction.svelte";
export let neuron: SnsNeuron;
export let feeE8s: bigint;
</script>

<Section testId="sns-neuron-maturity-section-component">
Expand All @@ -20,7 +21,7 @@
</p>
<ul class="content">
<SnsStakedMaturityItemAction {neuron} />
<SnsAvailableMaturityItemAction {neuron} />
<SnsAvailableMaturityItemAction {neuron} {feeE8s} />
<SnsViewActiveDisbursementsItemAction {neuron} />
</ul>
</Section>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
<script lang="ts">
import { hasEnoughMaturityToStakeOrDisburse } from "$lib/utils/sns-neuron.utils";
import { hasEnoughMaturityToDisburse } from "$lib/utils/sns-neuron.utils";
import { openSnsNeuronModal } from "$lib/utils/modals.utils";
import type { SnsNeuron } from "@dfinity/sns";
import DisburseMaturityButton from "$lib/components/neuron-detail/actions/DisburseMaturityButton.svelte";
import { replacePlaceholders } from "$lib/utils/i18n.utils";
import { formatToken } from "$lib/utils/token.utils";
import { i18n } from "$lib/stores/i18n";
export let neuron: SnsNeuron;
export let feeE8s: bigint;
let enoughMaturity: boolean;
$: enoughMaturity = hasEnoughMaturityToStakeOrDisburse(neuron);
$: enoughMaturity = hasEnoughMaturityToDisburse({ neuron, feeE8s });
let disabledText: string | undefined = undefined;
$: disabledText = !enoughMaturity
? replacePlaceholders(
$i18n.neuron_detail.disburse_maturity_disabled_tooltip,
{ $fee: formatToken({ value: feeE8s }) }
)
: undefined;
const showModal = () => openSnsNeuronModal({ type: "disburse-maturity" });
</script>

<DisburseMaturityButton {enoughMaturity} on:click={showModal} />
<DisburseMaturityButton {disabledText} on:click={showModal} />
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script lang="ts">
import { hasEnoughMaturityToStakeOrDisburse } from "$lib/utils/sns-neuron.utils";
import { openSnsNeuronModal } from "$lib/utils/modals.utils";
import type { SnsNeuron } from "@dfinity/sns";
import StakeMaturityButton from "$lib/components/neuron-detail/actions/StakeMaturityButton.svelte";
import { hasEnoughMaturityToStake } from "$lib/utils/sns-neuron.utils";
export let neuron: SnsNeuron;
let enoughMaturity: boolean;
$: enoughMaturity = hasEnoughMaturityToStakeOrDisburse(neuron);
$: enoughMaturity = hasEnoughMaturityToStake(neuron);
const showModal = () => openSnsNeuronModal({ type: "stake-maturity" });
</script>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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. <a href=\"https://wiki.internetcomputer.org/wiki/NNS_neuron_operations_related_to_maturity\" rel=\"noopener noreferrer\" aria-label=\"Find more information about the new stake maturity\" target=\"_blank\">Learn more</a>.",
"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?",
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/lib/pages/SnsNeuronDetail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,10 @@
{token}
/>
<Separator spacing="none" />
<SnsNeuronMaturitySection neuron={$selectedSnsNeuronStore.neuron} />
<SnsNeuronMaturitySection
neuron={$selectedSnsNeuronStore.neuron}
feeE8s={transactionFee}
/>
<Separator spacing="none" />
<SnsNeuronAdvancedSection
neuron={$selectedSnsNeuronStore.neuron}
Expand Down
15 changes: 14 additions & 1 deletion frontend/src/lib/utils/sns-neuron.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,10 +500,23 @@ export const formattedTotalMaturity = (neuron: SnsNeuron): string =>
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});

Expand Down Expand Up @@ -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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,25 @@
*/

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,
maturity: 214_000_000n,
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,
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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 () => {
Expand Down
45 changes: 39 additions & 6 deletions frontend/src/tests/lib/utils/sns-neuron.utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import {
getSnsNeuronStake,
getSnsNeuronState,
getSnsNeuronVote,
hasEnoughMaturityToStakeOrDisburse,
hasEnoughMaturityToDisburse,
hasEnoughMaturityToStake,
hasEnoughStakeToSplit,
hasPermissionToDisburse,
hasPermissionToDisburseMaturity,
Expand Down Expand Up @@ -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", () => {
Expand All @@ -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);
});
});

Expand Down
Loading

0 comments on commit c5114aa

Please sign in to comment.