From d5686fd91ad7c06969eb9300ce3d12833ee61e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Mon, 18 Sep 2023 11:38:00 +0200 Subject: [PATCH 1/9] GIX-1890: Check amount with transaction fee --- .../NeuronSelectPercentage.svelte | 32 ++++++++++++++----- .../neurons/DisburseMaturityModal.svelte | 22 ++++++++++++- .../neurons/SnsDisburseMaturityModal.svelte | 8 +++-- .../sns/SnsDisburseMaturityModal.spec.ts | 20 ++++++++++++ 4 files changed, 70 insertions(+), 12 deletions(-) diff --git a/frontend/src/lib/components/neuron-detail/NeuronSelectPercentage.svelte b/frontend/src/lib/components/neuron-detail/NeuronSelectPercentage.svelte index 8aad36b5299..6ebaebf3427 100644 --- a/frontend/src/lib/components/neuron-detail/NeuronSelectPercentage.svelte +++ b/frontend/src/lib/components/neuron-detail/NeuronSelectPercentage.svelte @@ -6,11 +6,14 @@ import TestIdWrapper from "$lib/components/common/TestIdWrapper.svelte"; import { formatMaturity } from "$lib/utils/neuron.utils"; import { replacePlaceholders } from "$lib/utils/i18n.utils"; + import { nonNullish } from "@dfinity/utils"; + import Tooltip from "../ui/Tooltip.svelte"; export let availableMaturityE8s: bigint; export let percentage: number; export let buttonText: string; export let disabled = false; + export let disabledText: string | undefined = undefined; let selectedMaturityE8s: bigint; $: selectedMaturityE8s = (availableMaturityE8s * BigInt(percentage)) / 100n; @@ -59,14 +62,27 @@ - + {#if nonNullish(disabledText)} + + + + {:else} + + {/if} diff --git a/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte b/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte index 745e2088ff2..8311767ccb7 100644 --- a/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte +++ b/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte @@ -17,6 +17,9 @@ export let availableMaturityE8s: bigint; export let tokenSymbol: string; + // 99% of users will disburse more than the transaction fee. + // We don't want a possible error fetching the fee to disrupt the whole flow. + export let transactionFeeE8s = 0n; const steps: WizardSteps = [ { @@ -33,6 +36,22 @@ let modal: WizardModal; let percentageToDisburse = 0; + let selectedMaturityE8s: bigint; + $: selectedMaturityE8s = + (availableMaturityE8s * BigInt(percentageToDisburse)) / 100n; + + let disableDisburse = false; + $: disableDisburse = selectedMaturityE8s < transactionFeeE8s; + + // Show the text only if the selected percentage is greater than 0. + let disabledText: string | undefined = undefined; + $: disabledText = + disableDisburse && percentageToDisburse > 0 + ? replacePlaceholders( + $i18n.neuron_detail.disburse_maturity_disabled_tooltip, + { $fee: formatToken({ value: transactionFeeE8s }) } + ) + : undefined; const dispatcher = createEventDispatcher(); const disburseNeuronMaturity = () => @@ -74,7 +93,8 @@ on:nnsSelectPercentage={goToConfirm} on:nnsCancel={close} bind:percentage={percentageToDisburse} - disabled={percentageToDisburse === 0} + disabled={disableDisburse} + {disabledText} >
diff --git a/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte b/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte index 5f6bfe3b874..6c3041fdbd7 100644 --- a/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte +++ b/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte @@ -10,7 +10,8 @@ import { snsProjectMainAccountStore } from "$lib/derived/sns/sns-project-accounts.derived"; import { shortenWithMiddleEllipsis } from "$lib/utils/format.utils"; import { tokensStore } from "$lib/stores/tokens.store"; - import type { Token } from "@dfinity/utils"; + import { selectedUniverseIdStore } from "$lib/derived/selected-universe.derived"; + import type { IcrcTokenMetadata } from "$lib/types/icrc"; export let neuron: SnsNeuron; export let neuronId: SnsNeuronId; @@ -25,8 +26,8 @@ $snsProjectMainAccountStore?.identifier ?? "" ); - let token: Token | undefined; - $: token = $tokensStore[rootCanisterId.toText()]?.token; + let token: IcrcTokenMetadata | undefined; + $: token = $tokensStore[$selectedUniverseIdStore.toText()]?.token; const dispatcher = createEventDispatcher(); const close = () => dispatcher("nnsClose"); @@ -57,6 +58,7 @@ { expect(await po.isNextButtonDisabled()).toBe(false); }); + it("should disable next button if amount of maturity is less than transaction fee", async () => { + const fee = 100_000_000n; + const neuron = createMockSnsNeuron({ + id: [1], + maturity: fee * 2n, + }); + tokensStore.setToken({ + canisterId: rootCanisterId, + token: { + fee, + ...mockSnsToken, + }, + }); + // Maturity is 2x the fee, so 10% of maturity is not enough to cover the fee + const percentage = 10; + const po = await renderSnsDisburseMaturityModal(neuron); + await po.setPercentage(percentage); + expect(await po.isNextButtonDisabled()).toBe(false); + }); + it("should display selected percentage and total maturity", async () => { const neuron = createMockSnsNeuron({ id: [1], From 82faec09d445b984f6b42bfc76a3f1986d3b16f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Mon, 18 Sep 2023 16:49:12 +0200 Subject: [PATCH 2/9] GIX-1890: Improve copy --- frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte | 4 ++-- frontend/src/lib/types/i18n.d.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte b/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte index 8311767ccb7..ff262ce5c05 100644 --- a/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte +++ b/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte @@ -48,8 +48,8 @@ $: disabledText = disableDisburse && percentageToDisburse > 0 ? replacePlaceholders( - $i18n.neuron_detail.disburse_maturity_disabled_tooltip, - { $fee: formatToken({ value: transactionFeeE8s }) } + $i18n.neuron_detail.disburse_maturity_modal_disabled_tooltip, + { $amount: formatToken({ value: transactionFeeE8s }) } ) : undefined; diff --git a/frontend/src/lib/types/i18n.d.ts b/frontend/src/lib/types/i18n.d.ts index 3d2f9f9b1dd..9a07da27e9e 100644 --- a/frontend/src/lib/types/i18n.d.ts +++ b/frontend/src/lib/types/i18n.d.ts @@ -635,6 +635,7 @@ interface I18nNeuron_detail { spawn: string; stake_maturity_disabled_tooltip: string; disburse_maturity_disabled_tooltip: string; + disburse_maturity_modal_disabled_tooltip: string; stake_maturity_tooltip: string; start_dissolve_description: string; stop_dissolve_description: string; From 33e2b74f9f0620207eb8e346c491c6db1769066b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Mon, 18 Sep 2023 17:02:23 +0200 Subject: [PATCH 3/9] GIX-1890: Fix after rebasing --- .../src/lib/modals/neurons/DisburseMaturityModal.svelte | 6 +++--- .../lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte b/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte index ff262ce5c05..2eb902c1a2d 100644 --- a/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte +++ b/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte @@ -19,7 +19,7 @@ export let tokenSymbol: string; // 99% of users will disburse more than the transaction fee. // We don't want a possible error fetching the fee to disrupt the whole flow. - export let transactionFeeE8s = 0n; + export let minimumAmountE8s = 0n; const steps: WizardSteps = [ { @@ -41,7 +41,7 @@ (availableMaturityE8s * BigInt(percentageToDisburse)) / 100n; let disableDisburse = false; - $: disableDisburse = selectedMaturityE8s < transactionFeeE8s; + $: disableDisburse = selectedMaturityE8s < minimumAmountE8s; // Show the text only if the selected percentage is greater than 0. let disabledText: string | undefined = undefined; @@ -49,7 +49,7 @@ disableDisburse && percentageToDisburse > 0 ? replacePlaceholders( $i18n.neuron_detail.disburse_maturity_modal_disabled_tooltip, - { $amount: formatToken({ value: transactionFeeE8s }) } + { $amount: formatToken({ value: minimumAmountE8s }) } ) : undefined; diff --git a/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte b/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte index 6c3041fdbd7..8265fd0fc7a 100644 --- a/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte +++ b/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte @@ -10,7 +10,6 @@ import { snsProjectMainAccountStore } from "$lib/derived/sns/sns-project-accounts.derived"; import { shortenWithMiddleEllipsis } from "$lib/utils/format.utils"; import { tokensStore } from "$lib/stores/tokens.store"; - import { selectedUniverseIdStore } from "$lib/derived/selected-universe.derived"; import type { IcrcTokenMetadata } from "$lib/types/icrc"; export let neuron: SnsNeuron; @@ -27,7 +26,7 @@ ); let token: IcrcTokenMetadata | undefined; - $: token = $tokensStore[$selectedUniverseIdStore.toText()]?.token; + $: token = $tokensStore[rootCanisterId.toText()]?.token; const dispatcher = createEventDispatcher(); const close = () => dispatcher("nnsClose"); @@ -58,7 +57,7 @@ Date: Mon, 18 Sep 2023 17:05:08 +0200 Subject: [PATCH 4/9] GIX-1890 --- .../src/lib/modals/neurons/DisburseMaturityModal.svelte | 5 ++--- .../lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte | 7 ++++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte b/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte index 2eb902c1a2d..05ac35b4084 100644 --- a/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte +++ b/frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte @@ -17,9 +17,8 @@ export let availableMaturityE8s: bigint; export let tokenSymbol: string; - // 99% of users will disburse more than the transaction fee. - // We don't want a possible error fetching the fee to disrupt the whole flow. - export let minimumAmountE8s = 0n; + + export let minimumAmountE8s: bigint; const steps: WizardSteps = [ { diff --git a/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte b/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte index 8265fd0fc7a..06022fbe93e 100644 --- a/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte +++ b/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte @@ -28,6 +28,11 @@ let token: IcrcTokenMetadata | undefined; $: token = $tokensStore[rootCanisterId.toText()]?.token; + // 99% of users will disburse more than the transaction fee. + // We don't want a possible error fetching the fee to disrupt the whole flow. + let minimumAmountE8s = 0n; + $: minimumAmountE8s = token?.fee ?? 0n; + const dispatcher = createEventDispatcher(); const close = () => dispatcher("nnsClose"); @@ -57,7 +62,7 @@ Date: Tue, 19 Sep 2023 07:33:12 +0200 Subject: [PATCH 5/9] GIX-1890: Add min to disburse based on moduation --- .../actions/SpawnNeuronButton.svelte | 18 ++++++++++++------ .../src/lib/constants/neurons.constants.ts | 2 +- .../neurons/SnsDisburseMaturityModal.svelte | 13 ++++++++----- frontend/src/lib/utils/neuron.utils.ts | 7 +++++-- frontend/src/lib/utils/sns-neuron.utils.ts | 9 +++++++++ .../tests/lib/utils/sns-neuron.utils.spec.ts | 11 +++++++++++ 6 files changed, 46 insertions(+), 14 deletions(-) diff --git a/frontend/src/lib/components/neuron-detail/actions/SpawnNeuronButton.svelte b/frontend/src/lib/components/neuron-detail/actions/SpawnNeuronButton.svelte index 12181e1a53b..a2ca51ce2a8 100644 --- a/frontend/src/lib/components/neuron-detail/actions/SpawnNeuronButton.svelte +++ b/frontend/src/lib/components/neuron-detail/actions/SpawnNeuronButton.svelte @@ -3,7 +3,7 @@ import { E8S_PER_ICP } from "$lib/constants/icp.constants"; import { MIN_NEURON_STAKE, - SPAWN_VARIANCE_PERCENTAGE, + MATURITY_MODULATION_VARIANCE_PERCENTAGE, } from "$lib/constants/neurons.constants"; import { i18n } from "$lib/stores/i18n"; import { formatNumber, formatPercentage } from "$lib/utils/format.utils"; @@ -49,17 +49,22 @@ $i18n.neuron_detail.spawn_neuron_disabled_tooltip, { $amount: formatNumber( - MIN_NEURON_STAKE / E8S_PER_ICP / SPAWN_VARIANCE_PERCENTAGE, + MIN_NEURON_STAKE / + E8S_PER_ICP / + MATURITY_MODULATION_VARIANCE_PERCENTAGE, { minFraction: 4, maxFraction: 4 } ), $min: formatNumber(MIN_NEURON_STAKE / E8S_PER_ICP, { minFraction: 0, maxFraction: 0, }), - $varibility: formatPercentage(SPAWN_VARIANCE_PERCENTAGE, { - minFraction: 0, - maxFraction: 0, - }), + $varibility: formatPercentage( + MATURITY_MODULATION_VARIANCE_PERCENTAGE, + { + minFraction: 0, + maxFraction: 0, + } + ), } )} > @@ -69,3 +74,4 @@ {/if} +MATURITY_MODULATION_VARIANCE_PERCENTAGEMATURITY_MODULATION_VARIANCE_PERCENTAGE diff --git a/frontend/src/lib/constants/neurons.constants.ts b/frontend/src/lib/constants/neurons.constants.ts index adb9db8e1a7..3639eb5826f 100644 --- a/frontend/src/lib/constants/neurons.constants.ts +++ b/frontend/src/lib/constants/neurons.constants.ts @@ -6,7 +6,7 @@ import { E8S_PER_ICP } from "./icp.constants"; export const MAX_NEURONS_MERGED = 2; export const MIN_NEURON_STAKE = E8S_PER_ICP; export const MAX_CONCURRENCY = 10; -export const SPAWN_VARIANCE_PERCENTAGE = 0.95; +export const MATURITY_MODULATION_VARIANCE_PERCENTAGE = 0.95; // Neuron ids are random u64. Max digits of a u64 is 20. export const MAX_NEURON_ID_DIGITS = 20; diff --git a/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte b/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte index 06022fbe93e..e1f6cd75ec1 100644 --- a/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte +++ b/frontend/src/lib/modals/sns/neurons/SnsDisburseMaturityModal.svelte @@ -5,7 +5,10 @@ import type { SnsNeuron, SnsNeuronId } from "@dfinity/sns"; import type { Principal } from "@dfinity/principal"; import { disburseMaturity as disburseMaturityService } from "$lib/services/sns-neurons.services"; - import { formattedMaturity } from "$lib/utils/sns-neuron.utils"; + import { + formattedMaturity, + minimumAmountToDisburseMaturity, + } from "$lib/utils/sns-neuron.utils"; import DisburseMaturityModal from "$lib/modals/neurons/DisburseMaturityModal.svelte"; import { snsProjectMainAccountStore } from "$lib/derived/sns/sns-project-accounts.derived"; import { shortenWithMiddleEllipsis } from "$lib/utils/format.utils"; @@ -28,10 +31,10 @@ let token: IcrcTokenMetadata | undefined; $: token = $tokensStore[rootCanisterId.toText()]?.token; - // 99% of users will disburse more than the transaction fee. - // We don't want a possible error fetching the fee to disrupt the whole flow. - let minimumAmountE8s = 0n; - $: minimumAmountE8s = token?.fee ?? 0n; + let minimumAmountE8s: bigint; + // Token is loaded with all the projects from the aggregator. + // Therefore, if the user made it here, it's present. + $: minimumAmountE8s = minimumAmountToDisburseMaturity(token?.fee ?? 0n); const dispatcher = createEventDispatcher(); const close = () => dispatcher("nnsClose"); diff --git a/frontend/src/lib/utils/neuron.utils.ts b/frontend/src/lib/utils/neuron.utils.ts index 1994116d1c8..f805448f70b 100644 --- a/frontend/src/lib/utils/neuron.utils.ts +++ b/frontend/src/lib/utils/neuron.utils.ts @@ -12,9 +12,9 @@ import { import { AGE_MULTIPLIER, DISSOLVE_DELAY_MULTIPLIER, + MATURITY_MODULATION_VARIANCE_PERCENTAGE, MAX_NEURONS_MERGED, MIN_NEURON_STAKE, - SPAWN_VARIANCE_PERCENTAGE, TOPICS_TO_FOLLOW_NNS, } from "$lib/constants/neurons.constants"; import { DEPRECATED_TOPICS } from "$lib/constants/proposals.constants"; @@ -499,7 +499,10 @@ export const isEnoughMaturityToSpawn = ({ const maturitySelected = Math.floor( (Number(fullNeuron.maturityE8sEquivalent) * percentage) / 100 ); - return maturitySelected >= MIN_NEURON_STAKE / SPAWN_VARIANCE_PERCENTAGE; + return ( + maturitySelected >= + MIN_NEURON_STAKE / MATURITY_MODULATION_VARIANCE_PERCENTAGE + ); }; export const isSpawning = (neuron: NeuronInfo): boolean => diff --git a/frontend/src/lib/utils/sns-neuron.utils.ts b/frontend/src/lib/utils/sns-neuron.utils.ts index 01abda4999a..2e3ba83cd6c 100644 --- a/frontend/src/lib/utils/sns-neuron.utils.ts +++ b/frontend/src/lib/utils/sns-neuron.utils.ts @@ -1,3 +1,4 @@ +import { MATURITY_MODULATION_VARIANCE_PERCENTAGE } from "$lib/constants/neurons.constants"; import { HOTKEY_PERMISSIONS, MANAGE_HOTKEY_PERMISSIONS, @@ -981,3 +982,11 @@ export const totalDisbursingMaturity = ({ (acc, disbursement) => acc + disbursement.amount_e8s, BigInt(0) ); + +/** + * The governance canister checks that the amount to disburse in the worst case (of the maturity modulation) is bigger than the transaction fee. + * + * Source: https://sourcegraph.com/github.com/dfinity/ic/-/blob/rs/sns/governance/src/governance.rs?L1651 + */ +export const minimumAmountToDisburseMaturity = (fee: bigint): bigint => + BigInt(Math.round(Number(fee) / MATURITY_MODULATION_VARIANCE_PERCENTAGE)); 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 d5aa4c5f696..f225032dece 100644 --- a/frontend/src/tests/lib/utils/sns-neuron.utils.spec.ts +++ b/frontend/src/tests/lib/utils/sns-neuron.utils.spec.ts @@ -47,6 +47,7 @@ import { isUserHotkey, isVesting, minNeuronSplittable, + minimumAmountToDisburseMaturity, needsRefresh, neuronAge, nextMemo, @@ -2492,4 +2493,14 @@ describe("sns-neuron utils", () => { expect(totalDisbursingMaturity(neuron)).toBe(0n); }); }); + + describe("minimumAmountToDisburseMaturity", () => { + it("returns worst case of maturity modulation", () => { + expect(minimumAmountToDisburseMaturity(10_000n)).toBe(10526n); + }); + + it("returns 0 if fee is 0", () => { + expect(minimumAmountToDisburseMaturity(0n)).toBe(0n); + }); + }); }); From 27e9fd017bb1bd93d8f2b6a281dfb00b7e3716ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Tue, 19 Sep 2023 07:40:14 +0200 Subject: [PATCH 6/9] GIX-1890: Add missing i18n key --- frontend/src/lib/types/i18n.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/lib/types/i18n.d.ts b/frontend/src/lib/types/i18n.d.ts index 9a07da27e9e..3d2f9f9b1dd 100644 --- a/frontend/src/lib/types/i18n.d.ts +++ b/frontend/src/lib/types/i18n.d.ts @@ -635,7 +635,6 @@ interface I18nNeuron_detail { spawn: string; stake_maturity_disabled_tooltip: string; disburse_maturity_disabled_tooltip: string; - disburse_maturity_modal_disabled_tooltip: string; stake_maturity_tooltip: string; start_dissolve_description: string; stop_dissolve_description: string; From 2b74a21b420ef1474dec1e5efb5d29faac380e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lloren=C3=A7?= Date: Tue, 19 Sep 2023 08:04:00 +0200 Subject: [PATCH 7/9] GIX-1890: Apply new minimum --- .../actions/SnsDisburseMaturityButton.svelte | 9 +++++++-- .../src/lib/modals/neurons/DisburseMaturityModal.svelte | 4 ++-- frontend/src/lib/utils/sns-neuron.utils.ts | 5 +++-- .../actions/SnsDisburseMaturityButton.spec.ts | 4 ++-- frontend/src/tests/lib/utils/sns-neuron.utils.spec.ts | 8 ++++---- 5 files changed, 18 insertions(+), 12 deletions(-) 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 f7714fb75f1..7c406025bdb 100644 --- a/frontend/src/lib/components/sns-neuron-detail/actions/SnsDisburseMaturityButton.svelte +++ b/frontend/src/lib/components/sns-neuron-detail/actions/SnsDisburseMaturityButton.svelte @@ -1,5 +1,8 @@