From a6b9364fd63037ddde026c4c5e19c70fbc74cca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Thu, 7 Nov 2024 15:37:18 +0100 Subject: [PATCH] Add flows for updating an active validator --- .../src/popup/popupX/constants/routes.ts | 24 ++- .../Validator/Result/ValidationResult.tsx | 26 ++- .../Validator/Status/Status.tsx | 4 +- .../Validator/TransactionFlow.tsx | 152 +++++++++++++++++- .../pages/EarningRewards/Validator/Update.tsx | 34 ++++ .../popupX/pages/EarningRewards/i18n/en.ts | 13 +- .../src/popup/popupX/shell/Routes.tsx | 23 ++- 7 files changed, 259 insertions(+), 17 deletions(-) create mode 100644 packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Update.tsx diff --git a/packages/browser-wallet/src/popup/popupX/constants/routes.ts b/packages/browser-wallet/src/popup/popupX/constants/routes.ts index a303218ce..b2ce9afbf 100644 --- a/packages/browser-wallet/src/popup/popupX/constants/routes.ts +++ b/packages/browser-wallet/src/popup/popupX/constants/routes.ts @@ -198,12 +198,30 @@ export const relativeRoutes = { /** Configure existing delegator */ update: { path: 'update', + config: { + backTitle: i18n.t('x:earn.validator.update.backTitle'), + }, /** Update validator stake */ - stake: { path: 'stake' }, + stake: { + path: 'stake', + config: { + backTitle: i18n.t('x:earn.validator.update.step.backTitle'), + }, + }, /** Update validator pool settings */ - settings: { path: 'settings' }, + settings: { + path: 'settings', + config: { + backTitle: i18n.t('x:earn.validator.update.step.backTitle'), + }, + }, /** Update validator keys */ - keys: { path: 'keys' }, + keys: { + path: 'keys', + config: { + backTitle: i18n.t('x:earn.validator.update.step.backTitle'), + }, + }, }, /** Submit configure validator transaction */ submit: { diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Result/ValidationResult.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Result/ValidationResult.tsx index e00cdee6b..59259965a 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Result/ValidationResult.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Result/ValidationResult.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { AccountInfoBaker, AccountTransactionType, ConfigureBakerPayload, TransactionHash } from '@concordium/web-sdk'; import { Navigate, useLocation, Location, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -10,7 +10,12 @@ import Card from '@popup/popupX/shared/Card'; import { ensureDefined } from '@shared/utils/basic-helpers'; import { useBlockChainParametersAboveV0 } from '@popup/shared/BlockChainParametersProvider'; import { secondsToDaysRoundedDown } from '@shared/utils/time-helpers'; -import { useGetTransactionFee, useTransactionSubmit } from '@popup/shared/utils/transaction-helpers'; +import { + TransactionSubmitError, + TransactionSubmitErrorType, + useGetTransactionFee, + useTransactionSubmit, +} from '@popup/shared/utils/transaction-helpers'; import { cpStakingCooldown } from '@shared/utils/chain-parameters-helpers'; import { submittedTransactionRoute } from '@popup/popupX/constants/routes'; import Text from '@popup/popupX/shared/Text'; @@ -22,6 +27,7 @@ import { showValidatorOpenStatus, showValidatorRestake, } from '../util'; +import ErrorMessage from '@popup/popupX/shared/Form/ErrorMessage'; export type ValidationResultLocationState = { payload: ConfigureBakerPayload; @@ -36,6 +42,7 @@ export default function ValidationResult() { const { t } = useTranslation('x', { keyPrefix: 'earn.validator' }); const getCost = useGetTransactionFee(AccountTransactionType.ConfigureBaker); const accountInfo = ensureDefined(useSelectedAccountInfo(), 'No account selected'); + const [error, setError] = useState(); const parametersV1 = useBlockChainParametersAboveV0(); const submitTransaction = useTransactionSubmit(accountInfo.accountAddress, AccountTransactionType.ConfigureBaker); @@ -82,12 +89,16 @@ export default function ValidationResult() { if (fee === undefined) { throw Error('Fee could not be calculated'); } - const tx = await submitTransaction(state.payload, fee); - nav(submittedTransactionRoute(TransactionHash.fromHexString(tx))); + try { + const tx = await submitTransaction(state.payload, fee); + nav(submittedTransactionRoute(TransactionHash.fromHexString(tx))); + } catch (e) { + if (e instanceof Error) { + setError(e); + } + } }; - // TODO: - // [ ] Add the rest of the transaction fields return ( @@ -184,6 +195,9 @@ export default function ValidationResult() { /> + {error instanceof TransactionSubmitError && error.type === TransactionSubmitErrorType.InsufficientFunds && ( + {t('submit.error.insufficientFunds')} + )} diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Status/Status.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Status/Status.tsx index f2a815925..871457102 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Status/Status.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Status/Status.tsx @@ -53,6 +53,7 @@ export default function ValidatorStatus() { /> + @@ -94,12 +95,11 @@ export default function ValidatorStatus() { )} - nav(absoluteRoutes.settings.earn.delegator.update.path)} + onClick={() => nav(absoluteRoutes.settings.earn.validator.update.path)} /> ); } + +/** The props passed to a component from {@linkcode withChangeValidation} */ +type ChangeValidationProps = { + /** Handler function for when the flow steps have all been completed */ + onDone(values: ValidatorForm | ValidatorFormExisting): void; + /** The initial values for the flow, which will be either the existing validator properties on chain, or the values set previously in the flow. */ + initial: ValidatorForm | ValidatorFormExisting; +}; + +/** HOC for creating a flow for updating validator properties */ +function withChangeValidation(Flow: ComponentType) { + return function Component() { + const { state, pathname } = useLocation() as Location & { + state: ValidatorForm | ValidatorFormExisting | undefined; + }; + const accountInfo = useSelectedAccountInfo(); + const nav = useNavigate(); + const [noChangesNotice, setNoChangesNotice] = useState(false); + + if ( + accountInfo === undefined || + accountInfo.type !== AccountInfoType.Baker || + accountInfo.accountBaker.version === 0 + ) { + return null; + } + const { + accountBaker: { stakedAmount, restakeEarnings, bakerPoolInfo }, + } = accountInfo; + + const existing: ValidatorFormExisting = { + stake: { + amount: formatCcdAmount(stakedAmount), + restake: restakeEarnings, + }, + status: bakerPoolInfo.openStatus, + metadataUrl: bakerPoolInfo.metadataUrl, + commissions: bakerPoolInfo.commissionRates, + }; + + const initial = state ?? existing; + + const handleDone = (form: ValidatorForm) => { + const payload = configureValidatorFromForm(form, existing); + + if (Object.values(payload).every((v) => v === undefined)) { + setNoChangesNotice(true); + return; + } + + nav(pathname, { replace: true, state: form }); // Override current router entry with stateful version + + const submitDelegatorState: ValidationResultLocationState = { + payload, + type: 'change', + }; + nav(absoluteRoutes.settings.earn.validator.submit.path, { state: submitDelegatorState }); + }; + return ( + <> + setNoChangesNotice(false)} /> + + + ); + }; +} + +/** Flow for updating the stake of a validator */ +export const UpdateValidatorStakeTransactionFlow = withChangeValidation(({ initial, onDone }) => { + const chainParams = useBlockChainParametersAboveV0(); + const store = useState>(initial ?? {}); + const { t } = useTranslation('x', { keyPrefix: 'earn.validator.update' }); + + return ( + onDone={onDone} valueStore={store}> + {{ + stake: { + render(stepInitial, onNext) { + if (chainParams === undefined) { + return null; + } + return ( + + ); + }, + }, + }} + + ); +}); + +/** Flow for updating the pool settings of a validator */ +export const UpdateValidatorPoolSettingsTransactionFlow = withChangeValidation(({ initial, onDone }) => { + const chainParams = useBlockChainParametersAboveV0(); + const store = useState>(initial ?? {}); + + return ( + onDone={onDone} valueStore={store}> + {{ + status: { + render(stepInitial, onNext) { + return ; + }, + }, + commissions: { + render(stepInitial, onNext) { + if (chainParams === undefined) { + return null; + } + return ; + }, + }, + metadataUrl: { + render(stepInitial, onNext) { + return ; + }, + }, + }} + + ); +}); + +/** Flow for updating the keys associated with a validator */ +export const UpdateValidatorKeysTransactionFlow = withChangeValidation(({ initial, onDone }) => { + const store = useState>(initial ?? {}); + + return ( + onDone={onDone} valueStore={store}> + {{ + keys: { + render(stepInitial, onNext) { + return ; + }, + }, + }} + + ); +}); diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Update.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Update.tsx new file mode 100644 index 000000000..5c349b672 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Update.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; + +import Page from '@popup/popupX/shared/Page'; +import Text from '@popup/popupX/shared/Text'; +import Button from '@popup/popupX/shared/Button'; +import { relativeRoutes } from '@popup/popupX/constants/routes'; + +export default function ValidatorUpdate() { + const { t } = useTranslation('x', { keyPrefix: 'earn.validator.update' }); + const nav = useNavigate(); + + return ( + + + {t('description')} + + nav(relativeRoutes.settings.earn.validator.update.stake.path)} + /> + nav(relativeRoutes.settings.earn.validator.update.settings.path)} + /> + nav(relativeRoutes.settings.earn.validator.update.keys.path)} + /> + + + ); +} diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/i18n/en.ts b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/i18n/en.ts index 3510ada18..e63487bad 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/i18n/en.ts +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/i18n/en.ts @@ -192,14 +192,22 @@ const t = { notice: 'This will lock your validation amount. Amount is released after {{cooldown}} days from the time you remove or decrease your validation stake.', }, update: { - title: 'Update validator', + title: 'Update validation', + backTitle: 'Earning rewards', noChangesNotice: { title: 'No changes', description: 'The proposed transaction contains no changes compared to the current validation.', buttonBack: 'Go back', }, + description: 'Choose what you want to make changes to.', + buttonStake: 'Update validation stake', + buttonPoolSettings: 'Update pool settings', + buttonKeys: 'Update validator keys', lowerStakeNotice: 'Reducing your stake is subject to a cooldown period of {{cooldown}} days, in which the stake cannot be spent or transferred.', + step: { + backTitle: 'Update validation', + }, }, remove: { title: 'Remove validator', @@ -289,6 +297,9 @@ const t = { backTitle: 'Validation settings', sender: { label: 'Sender' }, fee: { label: 'Estimated transaction fee' }, + error: { + insufficientFunds: 'Insufficient funds on account', + }, button: 'Submit validation', }, }, diff --git a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx index c305633d7..a94d8cdae 100644 --- a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx @@ -36,8 +36,14 @@ import { } from '../pages/EarningRewards/Delegator/TransactionFlow'; import DelegatorStatus from '../pages/EarningRewards/Delegator/Status'; import ValidatorStatus from '../pages/EarningRewards/Validator/Status'; -import { RegisterValidatorTransactionFlow } from '../pages/EarningRewards/Validator/TransactionFlow'; +import { + RegisterValidatorTransactionFlow, + UpdateValidatorKeysTransactionFlow, + UpdateValidatorPoolSettingsTransactionFlow, + UpdateValidatorStakeTransactionFlow, +} from '../pages/EarningRewards/Validator/TransactionFlow'; import ValidationResult from '../pages/EarningRewards/Validator/Result/ValidationResult'; +import UpdateValidator from '../pages/EarningRewards/Validator/Update'; export default function Routes({ messagePromptHandlers }: { messagePromptHandlers: MessagePromptHandlersType }) { const { handleConnectionResponse } = messagePromptHandlers; @@ -126,6 +132,21 @@ export default function Routes({ messagePromptHandlers }: { messagePromptHandler element={} /> + + } /> + } + /> + } + /> + } + /> + } path={relativeRoutes.settings.earn.delegator.submit.path}