Skip to content

Commit

Permalink
Merge pull request #565 from Concordium/ui-update/change-validation
Browse files Browse the repository at this point in the history
Add flows for updating an active validator
  • Loading branch information
soerenbf authored Nov 8, 2024
2 parents 525344a + a6b9364 commit 22e4b79
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 17 deletions.
24 changes: 21 additions & 3 deletions packages/browser-wallet/src/popup/popupX/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -22,6 +27,7 @@ import {
showValidatorOpenStatus,
showValidatorRestake,
} from '../util';
import ErrorMessage from '@popup/popupX/shared/Form/ErrorMessage';

Check failure on line 30 in packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Validator/Result/ValidationResult.tsx

View workflow job for this annotation

GitHub Actions / lint

`@popup/popupX/shared/Form/ErrorMessage` import should occur before import of `../util`

export type ValidationResultLocationState = {
payload: ConfigureBakerPayload;
Expand All @@ -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<Error>();

const parametersV1 = useBlockChainParametersAboveV0();
const submitTransaction = useTransactionSubmit(accountInfo.accountAddress, AccountTransactionType.ConfigureBaker);
Expand Down Expand Up @@ -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 (
<Page className="validation-result-container">
<Page.Top heading={title} />
Expand Down Expand Up @@ -184,6 +195,9 @@ export default function ValidationResult() {
/>
</Card.Row>
</Card>
{error instanceof TransactionSubmitError && error.type === TransactionSubmitErrorType.InsufficientFunds && (
<ErrorMessage className="m-t-10 text-center">{t('submit.error.insufficientFunds')}</ErrorMessage>
)}
<Page.Footer>
<Button.Main onClick={submit} label={t('submit.button')} className="m-t-20" />
</Page.Footer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export default function ValidatorStatus() {
/>
</Card.Row>
</Card>
<AccountCooldowns cooldowns={accountCooldowns} />
<Card className="validator-status__info">
<Card.Row>
<Card.RowDetails title={t('values.id.label')} value={accountBaker.bakerId.toString()} />
Expand Down Expand Up @@ -94,12 +95,11 @@ export default function ValidatorStatus() {
</Card.Row>
)}
</Card>
<AccountCooldowns cooldowns={accountCooldowns} />
<Page.Footer>
<Button.Main
className="m-t-10"
label={t('status.buttonUpdate')}
onClick={() => nav(absoluteRoutes.settings.earn.delegator.update.path)}
onClick={() => nav(absoluteRoutes.settings.earn.validator.update.path)}
/>
<Button.Main
label={t('status.buttonStop')}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
/* eslint-disable react/destructuring-assignment */
import React, { useCallback, useState } from 'react';
import React, { ComponentType, useCallback, useState } from 'react';
import { Location, useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { AccountInfoType } from '@concordium/web-sdk';

import { absoluteRoutes } from '@popup/popupX/constants/routes';
import MultiStepForm from '@popup/shared/MultiStepForm';
import FullscreenNotice, { FullscreenNoticeProps } from '@popup/popupX/shared/FullscreenNotice';
import Page from '@popup/popupX/shared/Page';
import Button from '@popup/popupX/shared/Button';
import { useBlockChainParametersAboveV0 } from '@popup/shared/BlockChainParametersProvider';
import { useSelectedAccountInfo } from '@popup/shared/AccountInfoListenerContext/AccountInfoListenerContext';
import { formatCcdAmount } from '@popup/popupX/shared/utils/helpers';

import { ValidatorForm, configureValidatorFromForm } from './util';
import { ValidatorForm, ValidatorFormExisting, configureValidatorFromForm } from './util';
import ValidatorStake from './Stake';
import { type ValidationResultLocationState } from './Result';
import OpenPool from './OpenPool';
import Keys from './Keys';
import Metadata from './Metadata';
import Commissions from './Commissions';

// TODO: use this when implementing update flows
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function NoChangesNotice(props: FullscreenNoticeProps) {
const { t } = useTranslation('x', { keyPrefix: 'earn.validator.update.noChangesNotice' });
return (
Expand Down Expand Up @@ -104,3 +105,146 @@ export function RegisterValidatorTransactionFlow() {
</MultiStepForm>
);
}

/** 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<ChangeValidationProps>) {
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 (
<>
<NoChangesNotice open={noChangesNotice} onClose={() => setNoChangesNotice(false)} />
<Flow initial={initial} onDone={handleDone} />
</>
);
};
}

/** Flow for updating the stake of a validator */
export const UpdateValidatorStakeTransactionFlow = withChangeValidation(({ initial, onDone }) => {
const chainParams = useBlockChainParametersAboveV0();
const store = useState<Partial<ValidatorForm>>(initial ?? {});
const { t } = useTranslation('x', { keyPrefix: 'earn.validator.update' });

return (
<MultiStepForm<ValidatorForm> onDone={onDone} valueStore={store}>
{{
stake: {
render(stepInitial, onNext) {
if (chainParams === undefined) {
return null;
}
return (
<ValidatorStake
title={t('title')}
onSubmit={onNext}
initialValues={stepInitial}
minStake={chainParams.minimumEquityCapital}
/>
);
},
},
}}
</MultiStepForm>
);
});

/** Flow for updating the pool settings of a validator */
export const UpdateValidatorPoolSettingsTransactionFlow = withChangeValidation(({ initial, onDone }) => {
const chainParams = useBlockChainParametersAboveV0();
const store = useState<Partial<ValidatorForm>>(initial ?? {});

return (
<MultiStepForm<ValidatorForm> onDone={onDone} valueStore={store}>
{{
status: {
render(stepInitial, onNext) {
return <OpenPool initial={stepInitial} onSubmit={onNext} />;
},
},
commissions: {
render(stepInitial, onNext) {
if (chainParams === undefined) {
return null;
}
return <Commissions initial={stepInitial} onSubmit={onNext} chainParams={chainParams} />;
},
},
metadataUrl: {
render(stepInitial, onNext) {
return <Metadata initial={stepInitial} onSubmit={onNext} />;
},
},
}}
</MultiStepForm>
);
});

/** Flow for updating the keys associated with a validator */
export const UpdateValidatorKeysTransactionFlow = withChangeValidation(({ initial, onDone }) => {
const store = useState<Partial<ValidatorForm>>(initial ?? {});

return (
<MultiStepForm<ValidatorForm> onDone={onDone} valueStore={store}>
{{
keys: {
render(stepInitial, onNext) {
return <Keys onSubmit={onNext} initial={stepInitial} />;
},
},
}}
</MultiStepForm>
);
});
Original file line number Diff line number Diff line change
@@ -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 (
<Page>
<Page.Top heading={t('title')} />
<Text.Capture>{t('description')}</Text.Capture>
<Page.Footer>
<Button.Main
label={t('buttonStake')}
onClick={() => nav(relativeRoutes.settings.earn.validator.update.stake.path)}
/>
<Button.Main
label={t('buttonPoolSettings')}
onClick={() => nav(relativeRoutes.settings.earn.validator.update.settings.path)}
/>
<Button.Main
label={t('buttonKeys')}
onClick={() => nav(relativeRoutes.settings.earn.validator.update.keys.path)}
/>
</Page.Footer>
</Page>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -297,6 +305,9 @@ const t = {
backTitle: 'Validation settings',
sender: { label: 'Sender' },
fee: { label: 'Estimated transaction fee' },
error: {
insufficientFunds: 'Insufficient funds on account',
},
button: 'Submit validation',
},
},
Expand Down
Loading

0 comments on commit 22e4b79

Please sign in to comment.