From a03daeac83e5f91f8afc41dedcd3cc160f85afce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Tue, 15 Oct 2024 12:10:28 +0200 Subject: [PATCH 01/20] Radio button cursor --- packages/browser-wallet/src/popup/popupX/shared/Form/Form.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/Form.scss b/packages/browser-wallet/src/popup/popupX/shared/Form/Form.scss index 39ba6da5d..963f2d221 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Form/Form.scss +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/Form.scss @@ -162,6 +162,7 @@ $handle-size: rem(20px); .form-radios-x { &__radio { + cursor: pointer; display: flex; align-items: center; gap: rem(16px); @@ -175,7 +176,6 @@ $handle-size: rem(20px); } .checkmark { - cursor: pointer; height: rem(20px); width: rem(20px); background-color: $color-grey-2; From 5034320055324936ae5aecb51d3f54230d1dffd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Tue, 15 Oct 2024 12:40:21 +0200 Subject: [PATCH 02/20] Move text to translations --- .../Delegator/Type/DelegationType.tsx | 34 +++++++++++-------- .../popupX/pages/EarningRewards/i18n/en.ts | 9 +++++ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx index 76d92be93..5861cdb74 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx @@ -1,26 +1,32 @@ import React from 'react'; import Radio from '@popup/popupX/shared/Form/Radios'; import Button from '@popup/popupX/shared/Button'; +import { Trans, useTranslation } from 'react-i18next'; +import Page from '@popup/popupX/shared/Page'; +import ExternalLink from '@popup/popupX/shared/ExternalLink'; export default function DelegationType() { + const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.target' }); return ( -
-
- Register Delegation -
- - You can delegate to an open pool of your choice, or you can stake using passive delegation. - + + + {t('description')}
- - + +
- Passive delegation is an alternative to delegation to a specific baker pool that has lower rewards. With - passive delegation you do not have to worry about the uptime or quality of a baker node. + + ), + }} + /> - For more info you can visit developer.concordium.software - -
+ + ); } 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 3f991dffe..4407af2f2 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 @@ -37,6 +37,15 @@ const t = { body: 'When you make a delegation to either type of pool, your delegation amount will be locked on your account.\n\nThis means that you cannot use the amount for anything while it is still locked in for delegation.\n\nIf you decrease your delegation amount or stop the delegation altogether, the amount will still be locked for a cool-down period.\n\nAs transactions cost a fee, it is important to take into consideration that you will need some unlocked funds on your public balance to pay the fee for unlocking your delegation amount again.', }, }, + target: { + title: 'Register delegation', + description: 'You can delegate to an open pool of your choice, or you can stake using passive delegation.', + radioValidatorLabel: 'Validator', + radioPassiveLabel: 'Passive', + passiveDelegationDescription: + 'Passive delegation is an alternative to delegation to a specific validator pool that has lower rewards. With passive delegation you do not have to worry about the uptime or quality of a baker node.\nFor more info you can visit <1>developer.concordium.software', + buttonContinue: 'Continue', + }, }, validator: { intro: { From 10613d233e03ef1f1c0cc9e38bda5006dce8e905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Wed, 16 Oct 2024 09:28:27 +0200 Subject: [PATCH 03/20] Revise delegation flow structure --- .../src/popup/popupX/constants/routes.ts | 21 +++++++------- .../EarningRewards/Baker/Intro/BakerIntro.tsx | 2 +- .../Delegator/Intro/DelegatorIntro.tsx | 7 +++-- .../Delegator/Register/index.ts | 1 - .../Delegator/RegisterDelegator.tsx | 17 +++++++++++ .../DelegatorStake.scss} | 0 .../DelegatorStake.tsx} | 2 +- .../EarningRewards/Delegator/Stake/index.ts | 1 + .../TransactionFlow/TransactionFlow.tsx | 5 ++++ .../Delegator/TransactionFlow/index.ts | 1 + .../pages/EarningRewards/EarningRewards.tsx | 4 +-- .../src/popup/popupX/shell/Routes.tsx | 29 ++++++------------- .../src/popup/popupX/styles/_elements.scss | 2 +- 13 files changed, 52 insertions(+), 40 deletions(-) delete mode 100644 packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Register/index.ts create mode 100644 packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/RegisterDelegator.tsx rename packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/{Register/RegisterDelegator.scss => Stake/DelegatorStake.scss} (100%) rename packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/{Register/RegisterDelegator.tsx => Stake/DelegatorStake.tsx} (98%) create mode 100644 packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/index.ts create mode 100644 packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx create mode 100644 packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/index.ts diff --git a/packages/browser-wallet/src/popup/popupX/constants/routes.ts b/packages/browser-wallet/src/popup/popupX/constants/routes.ts index bda42c69d..16220cbf1 100644 --- a/packages/browser-wallet/src/popup/popupX/constants/routes.ts +++ b/packages/browser-wallet/src/popup/popupX/constants/routes.ts @@ -146,9 +146,11 @@ export const relativeRoutes = { }, }, }, + /** Routes related to staking for the currently selected account */ earn: { path: 'earn', - baker: { + /** Validation related routes */ + validator: { path: 'baker', intro: { path: 'intro', @@ -159,23 +161,20 @@ export const relativeRoutes = { openPool: { path: 'openPool', }, - bakerKeys: { - path: 'bakerKeys', + keys: { + path: 'keys', }, }, + /** Delegation related routes */ delegator: { path: 'delegator', - intro: { - path: 'intro', - }, - type: { - path: 'type', - }, + /** Configure new delegator */ register: { path: 'register', }, - result: { - path: 'result', + /** Configure existing delegator */ + update: { + path: 'update', }, }, }, diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Baker/Intro/BakerIntro.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Baker/Intro/BakerIntro.tsx index a1f81d24e..ffc59a52a 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Baker/Intro/BakerIntro.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Baker/Intro/BakerIntro.tsx @@ -10,7 +10,7 @@ export default function BakerIntro() { const { t } = useTranslation('x', { keyPrefix: 'earn.validator.intro' }); return ( - nav(absoluteRoutes.settings.earn.baker.register.path)}> + nav(absoluteRoutes.settings.earn.validator.register.path)}> diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Intro/DelegatorIntro.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Intro/DelegatorIntro.tsx index 511222195..f26ea098d 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Intro/DelegatorIntro.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Intro/DelegatorIntro.tsx @@ -1,17 +1,18 @@ import React from 'react'; import Carousel from '@popup/popupX/shared/Carousel'; import { useNavigate } from 'react-router-dom'; -import { absoluteRoutes } from '@popup/popupX/constants/routes'; import Page from '@popup/popupX/shared/Page'; import { Trans, useTranslation } from 'react-i18next'; import ExternalLink from '@popup/popupX/shared/ExternalLink'; -export default function DelegatorIntro() { +type Props = { onDoneRoute: string }; + +export default function DelegatorIntro({ onDoneRoute }: Props) { const nav = useNavigate(); const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.intro' }); return ( - nav(absoluteRoutes.settings.earn.delegator.type.path)}> + nav(onDoneRoute)}> + } /> + } /> + + ); +} diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Register/RegisterDelegator.scss b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.scss similarity index 100% rename from packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Register/RegisterDelegator.scss rename to packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.scss diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Register/RegisterDelegator.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx similarity index 98% rename from packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Register/RegisterDelegator.tsx rename to packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx index d4d299202..376a812c3 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Register/RegisterDelegator.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx @@ -2,7 +2,7 @@ import React from 'react'; import Button from '@popup/popupX/shared/Button'; import { ToggleCheckbox } from '@popup/popupX/shared/Form/ToggleCheckbox'; -export default function RegisterDelegator() { +export default function DelegatorStake() { return (
diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/index.ts b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/index.ts new file mode 100644 index 000000000..e39a2bee3 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/index.ts @@ -0,0 +1 @@ +export { default } from './DelegatorStake'; diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx new file mode 100644 index 000000000..eac9e6ae2 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function TransactionFlow() { + return <>Transaction flow; +} diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/index.ts b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/index.ts new file mode 100644 index 000000000..63d943bbe --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/index.ts @@ -0,0 +1 @@ +export { default } from './TransactionFlow'; diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/EarningRewards.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/EarningRewards.tsx index 55c348d4c..24fc6c79e 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/EarningRewards.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/EarningRewards.tsx @@ -28,7 +28,7 @@ export default function EarningRewards() { {t('validatorDescription', { amount: displayAsCcd(bakingThreshold, false) })} - +
{t('validatorAction')} @@ -38,7 +38,7 @@ export default function EarningRewards() {
{t('delegationTitle')} {t('delegationDescription')} - +
{t('delegationAction')} diff --git a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx index df76d099f..f8bcd2ca4 100644 --- a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx @@ -20,15 +20,12 @@ import { IdSubmitted, IdCardsInfo, RequestIdentity, SetupPassword, Welcome } fro import ConnectedSites from '@popup/popupX/pages/ConnectedSites'; import EarningRewards from '@popup/popupX/pages/EarningRewards'; import { BakerIntro } from '@popup/popupX/pages/EarningRewards/Baker/Intro'; -import { DelegatorIntro } from '@popup/popupX/pages/EarningRewards/Delegator/Intro'; import { RegisterBaker } from '@popup/popupX/pages/EarningRewards/Baker/Register'; import { OpenPool } from '@popup/popupX/pages/EarningRewards/Baker/OpenPool'; import { BakerKeys } from '@popup/popupX/pages/EarningRewards/Baker/BakerKeys'; -import DelegationType from '@popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType'; import PrivateKey from '@popup/popupX/pages/PrivateKey'; import { RestoreIntro, RestoreResult } from '@popup/popupX/pages/Restore'; -import RegisterDelegator from '../pages/EarningRewards/Delegator/Register/RegisterDelegator'; -import DelegationResult from '../pages/EarningRewards/Delegator/Result/DelegationResult'; +import RegisterDelegator from '../pages/EarningRewards/Delegator/RegisterDelegator'; export default function Routes() { return ( @@ -91,31 +88,23 @@ export default function Routes() { } path={relativeRoutes.settings.about.path} /> } /> - - } path={relativeRoutes.settings.earn.baker.intro.path} /> + + } path={relativeRoutes.settings.earn.validator.intro.path} /> } - path={relativeRoutes.settings.earn.baker.register.path} + path={relativeRoutes.settings.earn.validator.register.path} /> - } path={relativeRoutes.settings.earn.baker.openPool.path} /> - } path={relativeRoutes.settings.earn.baker.bakerKeys.path} /> + } path={relativeRoutes.settings.earn.validator.openPool.path} /> + } path={relativeRoutes.settings.earn.validator.keys.path} /> - } - path={relativeRoutes.settings.earn.delegator.intro.path} - /> - } - path={relativeRoutes.settings.earn.delegator.type.path} - /> } - path={relativeRoutes.settings.earn.delegator.register.path} + path={`${relativeRoutes.settings.earn.delegator.register.path}/*`} /> } - path={relativeRoutes.settings.earn.delegator.result.path} + element={} // FIXME: change to update flow + path={`${relativeRoutes.settings.earn.delegator.update.path}/*`} /> diff --git a/packages/browser-wallet/src/popup/popupX/styles/_elements.scss b/packages/browser-wallet/src/popup/popupX/styles/_elements.scss index 09e36db8b..b7859e337 100644 --- a/packages/browser-wallet/src/popup/popupX/styles/_elements.scss +++ b/packages/browser-wallet/src/popup/popupX/styles/_elements.scss @@ -23,7 +23,7 @@ @import '../pages/EarningRewards/Baker/BakerKeys/BakerKeys'; @import '../pages/EarningRewards/Delegator/Intro/DelegatorIntro'; @import '../pages/EarningRewards/Delegator/Type/DelegationType'; -@import '../pages/EarningRewards/Delegator/Register/RegisterDelegator'; +@import '../pages/EarningRewards/Delegator/Stake/DelegatorStake'; @import '../pages/EarningRewards/Delegator/Result/DelegationResult'; @import '../page-layouts/MainLayout'; @import '../shared/Page/Page'; From 1d1abb75bebf887621ad930bea855f68db9eac2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Wed, 16 Oct 2024 11:11:35 +0200 Subject: [PATCH 04/20] Setup multi-step form structure for delegation configuration --- .../Delegator/Stake/DelegatorStake.tsx | 8 + .../EarningRewards/Delegator/Stake/index.ts | 2 +- .../TransactionFlow/TransactionFlow.tsx | 37 +++- .../Delegator/Type/DelegationType.tsx | 51 ++++- .../EarningRewards/Delegator/Type/index.ts | 2 +- .../src/popup/popupX/shared/Form/Form.tsx | 10 +- .../popupX/shared/Form/Radios/Radios.tsx | 12 +- .../popup/popupX/shared/Form/Radios/index.ts | 2 +- .../src/popup/popupX/shared/MultiStepForm.tsx | 180 ++++++++++++++++++ .../src/popup/popupX/shared/Page/Page.tsx | 5 +- 10 files changed, 286 insertions(+), 23 deletions(-) create mode 100644 packages/browser-wallet/src/popup/popupX/shared/MultiStepForm.tsx diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx index 376a812c3..4936e79a5 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx @@ -2,6 +2,14 @@ import React from 'react'; import Button from '@popup/popupX/shared/Button'; import { ToggleCheckbox } from '@popup/popupX/shared/Form/ToggleCheckbox'; +/** The form values for delegator stake configuration step */ +export type DelegatorStakeForm = { + /** in CCD */ + amount: string; + /** Whether to add rewards to the stake or not */ + redelegate: boolean; +}; + export default function DelegatorStake() { return (
diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/index.ts b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/index.ts index e39a2bee3..803d25b73 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/index.ts +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/index.ts @@ -1 +1 @@ -export { default } from './DelegatorStake'; +export { default, type DelegatorStakeForm } from './DelegatorStake'; diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx index eac9e6ae2..90c4cf0cf 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx @@ -1,5 +1,38 @@ -import React from 'react'; +import MultiStepForm from '@popup/popupX/shared/MultiStepForm'; +import React, { useCallback } from 'react'; +import { Location, useLocation, useNavigate } from 'react-router-dom'; +import DelegatorStake, { DelegatorStakeForm } from '../Stake'; +import DelegatorType, { DelegationTypeForm } from '../Type'; + +/** Represents the form data for a configure delegator transaction. */ +type DelegatorForm = { + /** The delegation target configuration */ + target: DelegationTypeForm; + /** The delegation stake configuration */ + stake: DelegatorStakeForm; +}; export default function TransactionFlow() { - return <>Transaction flow; + const { state: initialValues, pathname } = useLocation() as Location & { state: DelegatorForm | undefined }; + const nav = useNavigate(); + + const handleDone = useCallback( + (values: DelegatorForm) => { + nav(pathname, { replace: true, state: values }); // Override current router entry with stateful version + }, + [pathname] + ); + + return ( + onDone={handleDone} initialValues={initialValues}> + {{ + target: { + render: (initial, onNext) => , + }, + stake: { + render: () => , + }, + }} + + ); } diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx index 5861cdb74..f87fbad67 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx @@ -1,20 +1,55 @@ import React from 'react'; -import Radio from '@popup/popupX/shared/Form/Radios'; -import Button from '@popup/popupX/shared/Button'; +import { FormRadio } from '@popup/popupX/shared/Form/Radios'; import { Trans, useTranslation } from 'react-i18next'; import Page from '@popup/popupX/shared/Page'; import ExternalLink from '@popup/popupX/shared/ExternalLink'; +import { DelegationTargetType } from '@concordium/web-sdk'; +import Form, { useForm } from '@popup/popupX/shared/Form'; +import Button from '@popup/popupX/shared/Button'; + +/** Describes the form values for configuring the delegation target of a delegation transaction */ +export type DelegationTypeForm = { + /** The target for the delegation */ + target: DelegationTargetType; + /** The target baker ID - only relevant for target = {@linkcode DelegationTargetType.Baker} */ + bakerId?: string; +}; + +type Props = { + /** The initial values delegation configuration target step */ + initialValues: DelegationTypeForm | undefined; + /** The submit handler triggered when submitting the form in the step */ + onSubmit(values: DelegationTypeForm): void; +}; -export default function DelegationType() { +export default function DelegationType({ initialValues, onSubmit }: Props) { + const form = useForm({ + defaultValues: initialValues ?? { target: DelegationTargetType.PassiveDelegation }, + }); const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.target' }); + const submit = form.handleSubmit(onSubmit); return ( {t('description')} -
- - -
+ className="delegation-type__select-form" formMethods={form}> + {(f) => ( + <> + + + + )} + - +
); } diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/index.ts b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/index.ts index 9ae73aa32..c801c748c 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/index.ts +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/index.ts @@ -1 +1 @@ -export { default as DelegationType } from './DelegationType'; +export { default, type DelegationTypeForm } from './DelegationType'; diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/Form.tsx b/packages/browser-wallet/src/popup/popupX/shared/Form/Form.tsx index 065793425..5ec20f5ad 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Form/Form.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/Form.tsx @@ -9,7 +9,7 @@ import { useForm as useFormLib, FormProvider, } from 'react-hook-form'; -import { ClassName, Id } from 'wallet-common-helpers'; +import { ClassName, Id, noOp } from 'wallet-common-helpers'; const useFormDefaults: Pick = { mode: 'onTouched', @@ -26,9 +26,9 @@ export const useForm = ( type FormProps = Id & ClassName & { /** - * Submit handler, receiving the values of the form as arg. + * Submit handler, receiving the values of the form as arg. If left undefined, form submission is expected to be handled explicitly elsewhere. */ - onSubmit: SubmitHandler; + onSubmit?: SubmitHandler; /** * Optional form methods from 'react-hook-form' useForm, if form methods need to be accessed outside of the form. */ @@ -75,9 +75,11 @@ const Form = forwardRef( const internal = useForm({ defaultValues }); const methods = external ?? internal; + const submit = () => (onSubmit === undefined ? noOp : methods.handleSubmit(onSubmit)); + return ( -
+ {children(methods)}
diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/Radios/Radios.tsx b/packages/browser-wallet/src/popup/popupX/shared/Form/Radios/Radios.tsx index da2fbfb89..accf7ab6d 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Form/Radios/Radios.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/Radios/Radios.tsx @@ -1,5 +1,6 @@ import { RequiredControlledFieldProps } from '@popup/shared/Form/common/types'; -import React, { InputHTMLAttributes } from 'react'; +import React, { InputHTMLAttributes, forwardRef } from 'react'; +import { makeUncontrolled } from '../common/utils'; type RadioProps = Omit & Pick, 'onBlur' | 'onChange' | 'checked'> & { @@ -7,12 +8,15 @@ type RadioProps = Omit & id: string; }; -export default function Radio({ label, id, ...inputProps }: RadioProps) { +const Radio = forwardRef(({ id, label, ...inputProps }, ref) => { return ( ); -} +}); + +export default Radio; +export const FormRadio = makeUncontrolled(Radio); diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/Radios/index.ts b/packages/browser-wallet/src/popup/popupX/shared/Form/Radios/index.ts index 441fd6190..35a6fd975 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Form/Radios/index.ts +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/Radios/index.ts @@ -1 +1 @@ -export { default } from './Radios'; +export { default, FormRadio } from './Radios'; diff --git a/packages/browser-wallet/src/popup/popupX/shared/MultiStepForm.tsx b/packages/browser-wallet/src/popup/popupX/shared/MultiStepForm.tsx new file mode 100644 index 000000000..e73bc208b --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/shared/MultiStepForm.tsx @@ -0,0 +1,180 @@ +import React, { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react'; +import { Routes, useNavigate, Route, useLocation } from 'react-router-dom'; +import { isDefined, noOp, useUpdateEffect } from 'wallet-common-helpers'; + +const INDEX_ROUTE = '.'; + +export interface MultiStepFormPageProps { + /** + * Function to be triggered on page submission. Will take user to next page in the flow. + */ + onNext(values: V): void; + /** + * Initial values for substate. + */ + initial: V | undefined; + /** + * Accumulated values of entire flow (thus far) + */ + formValues: Partial; +} + +const makeFormPageObjects = >(children: FormChildren) => { + const keyPagePairs = Object.entries(children).filter(([, c]) => isDefined(c)); + + return keyPagePairs.map(([k, c]: [keyof F, FormChild], i) => ({ + substate: k, + render: c.render, + route: i === 0 ? INDEX_ROUTE : `${i}`, + })); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface FormChild { + /** + * Function to render page component responsible for letting user fill out the respective substate. + * This is a function to avoid anonymous components messing up render tree updates. + */ + render(initial: F[K] | undefined, onNext: (values: F[K]) => void, formValues: Partial): JSX.Element; +} + +export type FormChildren> = { + [K in keyof F]?: FormChild; +}; + +/** + * Helper type to generate type for children expected by MultiStepForm + */ +export type OrRenderValues, C extends FormChildren> = + | C + | ((values: Partial) => C); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ValidateValues> = (values: F) => keyof F | undefined; + +interface Props> { + /** + * Function to validate the transaction flow values as a whole. + * Return key of the substate containing the invalid field, or undefined if valid + */ + validate?: ValidateValues; + onDone(values: F): void; + onPageActive?(step: keyof F, values: Partial): void; + /** + * Pages of the transaction flow declared as a mapping of components to corresponding substate. + * Declaration order defines the order the pages are shown. + */ + children: OrRenderValues>; +} + +interface InternalValueStoreProps> extends Props { + /** + * Initial values for the form. + */ + initialValues?: F; +} + +interface ExternalValueStoreProps> extends Props { + /** + * Matches the return type of "useState" hook + */ + valueStore: [Partial, Dispatch>>]; +} + +/** + * Props for multi step form component. Can either use an internal or external value store, which simply matches the tuple returned from the "useState" hook + * + * @template F Type of the form as a whole. Each step in the form flow should correspond to a member on the type. + */ +export type MultiStepFormProps> = + | InternalValueStoreProps + | ExternalValueStoreProps; + +/** + * A component for spanning forms over multiple pages. This component doesn't render any UI, but merely handles collecting data from the different steps and routing between the steps. + * + * @template F Type of the form as a whole. Each step in the form flow should correspond to a member on the type. + * @component + * @example + * type Values = { + * first: { a: string; b: number; }; + * second: { c: boolean; }; + * }; + * + * > + * {{ + * first: { render: (initialValues, onNext) => }, + * second: { render: (initialValues, onNext) => }, + * }} + * + */ +export default function MultiStepForm< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + F extends Record +>(props: MultiStepFormProps) { + const { children, validate = () => undefined, onDone, onPageActive = noOp } = props; + const initialValues = (props as InternalValueStoreProps).initialValues ?? ({} as F); + const { pathname } = useLocation(); + const internalValueStore = useState>(initialValues); + const externalValueStore = (props as ExternalValueStoreProps).valueStore; + const [values, setValues] = externalValueStore ?? internalValueStore; + const nav = useNavigate(); + + const getChildren = useCallback( + (v: Partial) => (typeof children === 'function' ? children(v) : children), + [children] + ); + + const pages = useMemo(() => makeFormPageObjects(getChildren(values)), [getChildren, values]); + const currentPage = pages.find((p) => pathname.endsWith(p.route)) ?? pages[0]; + + useEffect(() => { + if (currentPage?.substate) { + onPageActive(currentPage?.substate, values); + } + }, [currentPage?.substate]); + + useUpdateEffect(() => { + throw new Error('Changing value store during the lifetime of MultiStepForm will result in errors.'); + }, [externalValueStore === undefined]); + + const handleNext = (substate: keyof F) => (v: Partial) => { + const newValues = { ...values, [substate]: v }; + setValues(newValues); + + const newPages = makeFormPageObjects(getChildren(newValues)); + const currentIndex = newPages.findIndex((p) => p.substate === substate); + + if (currentIndex === -1) { + // Could not find current page. Should not happen. + // TODO: Log error. + nav(INDEX_ROUTE, { replace: true }); + } else if (currentIndex !== newPages.length - 1) { + // From any page that isn't the last, to the next in line. + const { route } = newPages[currentIndex + 1] ?? {}; + nav(route); + } else { + // On final page. Do validation -> trigger done. + const invalidPage = pages.find((p) => p.substate === validate(newValues as F)); + + if (invalidPage) { + nav(invalidPage.route); + return; + } + + onDone(newValues as F); + } + }; + + return ( + + {pages.map(({ render, route, substate }) => + route === INDEX_ROUTE ? ( + + ) : ( + + ) + )} + + ); +} diff --git a/packages/browser-wallet/src/popup/popupX/shared/Page/Page.tsx b/packages/browser-wallet/src/popup/popupX/shared/Page/Page.tsx index 3c716c58d..104dd29fd 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Page/Page.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/Page/Page.tsx @@ -1,8 +1,9 @@ import React, { ReactNode } from 'react'; import Text from '@popup/popupX/shared/Text'; +import clsx from 'clsx'; -function PageRoot({ className, children }: { className: string; children: ReactNode }) { - return
{children}
; +function PageRoot({ className, children }: { className?: string; children: ReactNode }) { + return
{children}
; } function PageTop({ heading, children }: { heading?: ReactNode; children?: ReactNode }) { From 86c55c72525f640872c08cb96f6f16c96ef17676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Wed, 16 Oct 2024 14:39:28 +0200 Subject: [PATCH 05/20] Configure delegation target implementation --- .../Delegator/Type/DelegationType.tsx | 71 +++++++++++++++---- .../popupX/pages/EarningRewards/i18n/en.ts | 9 +++ .../src/popup/popupX/shared/Form/Form.scss | 18 ++++- 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx index f87fbad67..61bec1aad 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx @@ -1,11 +1,15 @@ import React from 'react'; import { FormRadio } from '@popup/popupX/shared/Form/Radios'; import { Trans, useTranslation } from 'react-i18next'; +import { Validate } from 'react-hook-form'; import Page from '@popup/popupX/shared/Page'; import ExternalLink from '@popup/popupX/shared/ExternalLink'; -import { DelegationTargetType } from '@concordium/web-sdk'; +import { DelegationTargetType, OpenStatusText } from '@concordium/web-sdk'; import Form, { useForm } from '@popup/popupX/shared/Form'; import Button from '@popup/popupX/shared/Button'; +import FormInput from '@popup/popupX/shared/Form/Input/Input'; +import { useAtomValue } from 'jotai'; +import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings'; /** Describes the form values for configuring the delegation target of a delegation transaction */ export type DelegationTypeForm = { @@ -23,16 +27,35 @@ type Props = { }; export default function DelegationType({ initialValues, onSubmit }: Props) { + const network = useAtomValue(networkConfigurationAtom); + const client = useAtomValue(grpcClientAtom); + const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.target' }); + const form = useForm({ defaultValues: initialValues ?? { target: DelegationTargetType.PassiveDelegation }, }); - const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.target' }); const submit = form.handleSubmit(onSubmit); + const target = form.watch('target'); + + const validateBakerId: Validate = async (value) => { + try { + const bakerId = BigInt(value!); // Unwrap is safe here, as it will always be defined. + const poolStatus = await client.getPoolInfo(bakerId); + + if (poolStatus.poolInfo?.openStatus !== OpenStatusText.OpenForAll) { + return t('inputValidatorId.errorClosed'); + } + return true; + } catch { + return t('inputValidatorId.errorNotValidator'); + } + }; + return ( {t('description')} - className="delegation-type__select-form" formMethods={form}> + className="delegation-type__select-form" formMethods={form} onSubmit={onSubmit}> {(f) => ( <> + {target === DelegationTargetType.Baker && ( + + )} )} - - ), - }} - /> + {target === DelegationTargetType.PassiveDelegation ? ( + + ), + }} + /> + ) : ( + , + }} + /> + )} 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 4407af2f2..b83a23787 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 @@ -42,6 +42,15 @@ const t = { description: 'You can delegate to an open pool of your choice, or you can stake using passive delegation.', radioValidatorLabel: 'Validator', radioPassiveLabel: 'Passive', + inputValidatorId: { + label: 'Enter validator pool ID', + errorRequired: 'Please specify a validator ID', + errorMin: 'Validator ID cannot be negative', + errorClosed: 'The validator pool is not open for delegation', + errorNotValidator: 'The specified ID is not a validator pool', + }, + validatorDelegationDescription: + 'If you don’t already know which validator pool you want to delegate an amount to, you can look for on <1>here.', passiveDelegationDescription: 'Passive delegation is an alternative to delegation to a specific validator pool that has lower rewards. With passive delegation you do not have to worry about the uptime or quality of a baker node.\nFor more info you can visit <1>developer.concordium.software', buttonContinue: 'Continue', diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/Form.scss b/packages/browser-wallet/src/popup/popupX/shared/Form/Form.scss index 963f2d221..fc6bb9914 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Form/Form.scss +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/Form.scss @@ -14,6 +14,10 @@ position: relative; display: block; + &:not(:last-of-type) { + margin-bottom: rem(8px); + } + &__field { background-color: $color-input-bg; color: $color-white; @@ -21,7 +25,6 @@ border: rem(1px) solid $color-grey-4; border-radius: rem(12px); padding: rem(28px) rem(14px) rem(12px); - margin-bottom: rem(8px); width: 100%; outline: none; font-size: rem(14px); @@ -33,6 +36,13 @@ &:focus { border-color: $color-mineral-2; } + + // Hide the buttons to increment/decrement + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + appearance: none; + margin: 0; + } } &__label { @@ -40,6 +50,7 @@ left: rem(14px); top: rem(8px); color: $color-mineral-3; + opacity: 0.5; } @include when-valid { @@ -169,7 +180,10 @@ $handle-size: rem(20px); background-color: rgba($color-grey-3, 0.3); padding: rem(22px); border-radius: rem(8px); - margin-bottom: rem(4px); + + &:not(:last-of-type) { + margin-bottom: rem(4px); + } input { display: none; From 5ac9eddc63c299b18a5d8081d6a989455dc1f220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Wed, 16 Oct 2024 16:07:30 +0200 Subject: [PATCH 06/20] Delegator stake configuration layout + text --- .../Delegator/Stake/DelegatorStake.scss | 15 +--- .../Delegator/Stake/DelegatorStake.tsx | 87 ++++++++++++------- .../TransactionFlow/TransactionFlow.tsx | 15 +++- .../Delegator/Type/DelegationType.scss | 2 +- .../Delegator/Type/DelegationType.tsx | 21 +++-- .../popupX/pages/EarningRewards/i18n/en.ts | 32 ++++++- .../popup/popupX/shared/Button/Button.scss | 1 - .../src/popup/popupX/shared/Button/Button.tsx | 2 +- .../src/popup/styles/util/_spacing.scss | 5 ++ 9 files changed, 123 insertions(+), 57 deletions(-) diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.scss b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.scss index c6a9b0c1b..94d8debdb 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.scss +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.scss @@ -1,23 +1,12 @@ .register-delegator-container { display: flex; flex-direction: column; - height: 100%; + min-height: 100%; padding-bottom: rem(32px); .register-delegator { - &__title { - display: flex; - flex-direction: column; - align-items: baseline; - justify-content: space-between; - margin-bottom: rem(16px); - - .capture__main_small { - padding-left: rem(6px); - } - } - &__token-card { + margin-top: rem(16px); background: $gradient-card-bg; border-radius: rem(16px); padding: rem(20px) rem(16px); diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx index 4936e79a5..fc043f090 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx @@ -1,6 +1,10 @@ import React from 'react'; import Button from '@popup/popupX/shared/Button'; import { ToggleCheckbox } from '@popup/popupX/shared/Form/ToggleCheckbox'; +import Page from '@popup/popupX/shared/Page'; +import { useTranslation } from 'react-i18next'; +import { DelegationTargetType } from '@concordium/web-sdk'; +import { useForm } from '@popup/popupX/shared/Form'; /** The form values for delegator stake configuration step */ export type DelegatorStakeForm = { @@ -10,53 +14,78 @@ export type DelegatorStakeForm = { redelegate: boolean; }; -export default function DelegatorStake() { +type Props = { + /** The title for the configuriation step */ + title: string; + /** The delegation target type */ + target: DelegationTargetType; + /** The initial values of the step, if any */ + initialValues?: DelegatorStakeForm; + /** The submit handler triggered when submitting the form in the step */ + onSubmit(values: DelegatorStakeForm): void; +}; + +export default function DelegatorStake({ title, target, initialValues, onSubmit }: Props) { + const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.stake' }); + const form = useForm({ + defaultValues: initialValues ?? { amount: '0.00', redelegate: true }, + }); + const submit = form.handleSubmit(onSubmit); + + // FIXME: hardcoded... + const account = 'Account 1 / 6gk...k7o'; + const balance = '17,800.00'; + const amount = '12,600.00'; // TODO: is it even feasible to render it like this when it's input?? + const fee = '0,32'; + const poolStake = '300,000.00'; + const poolCap = '56,400.00'; + return ( -
-
- Register Delegation - on Accout 1 / 6gk...k7o -
+ + + {t('selectedAccount', { account })}
- Token + {t('token.label')}
- CCD - 17,800 CCD available + {t('token.value')} + {t('token.balance', { balance })}
- Amount + {t('inputAmount.label')}
- 12,600.00 - Stake max. + {amount} + {t('inputAmount.buttonMax')}
- Estimated transaction fee: - 1,000.00 CCD + {t('fee.label')} + {t('fee.value', { amount: fee })}
-
-
- Current pool - 300,000.00 CCD -
-
- Pool limit - 56,400.66 CCD + {target === DelegationTargetType.Baker && ( +
+
+ {t('poolStake.label')} + {t('poolStake.value', { amount: poolStake })} +
+
+ {t('poolCap.label')} + {t('poolCap.value', { amount: poolCap })} +
-
+ )}
- Auto add rewards + {t('redelegate.label')}
- - I want to automatically add my baking rewards to my baker stake - + {t('redelegate.description')}
- -
+ + + +
); } diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx index 90c4cf0cf..7d4e92984 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx @@ -1,6 +1,7 @@ import MultiStepForm from '@popup/popupX/shared/MultiStepForm'; import React, { useCallback } from 'react'; import { Location, useLocation, useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import DelegatorStake, { DelegatorStakeForm } from '../Stake'; import DelegatorType, { DelegationTypeForm } from '../Type'; @@ -15,6 +16,7 @@ type DelegatorForm = { export default function TransactionFlow() { const { state: initialValues, pathname } = useLocation() as Location & { state: DelegatorForm | undefined }; const nav = useNavigate(); + const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.register' }); const handleDone = useCallback( (values: DelegatorForm) => { @@ -27,10 +29,19 @@ export default function TransactionFlow() { onDone={handleDone} initialValues={initialValues}> {{ target: { - render: (initial, onNext) => , + render: (initial, onNext) => ( + + ), }, stake: { - render: () => , + render: (initial, onNext, form) => { + if (form.target === undefined) { + nav('..'); + return null; + } + + return ; + }, }, }} diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.scss b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.scss index d716fe95f..f687044ae 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.scss +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.scss @@ -1,7 +1,7 @@ .delegation-type-container { display: flex; flex-direction: column; - height: 100%; + min-height: 100%; padding-bottom: rem(32px); .delegation-type { diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx index 61bec1aad..5b75e005e 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx @@ -14,28 +14,29 @@ import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings' /** Describes the form values for configuring the delegation target of a delegation transaction */ export type DelegationTypeForm = { /** The target for the delegation */ - target: DelegationTargetType; + type: DelegationTargetType; /** The target baker ID - only relevant for target = {@linkcode DelegationTargetType.Baker} */ bakerId?: string; }; type Props = { + title: string; /** The initial values delegation configuration target step */ - initialValues: DelegationTypeForm | undefined; + initialValues?: DelegationTypeForm; /** The submit handler triggered when submitting the form in the step */ onSubmit(values: DelegationTypeForm): void; }; -export default function DelegationType({ initialValues, onSubmit }: Props) { +export default function DelegationType({ initialValues, onSubmit, title }: Props) { const network = useAtomValue(networkConfigurationAtom); const client = useAtomValue(grpcClientAtom); const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.target' }); const form = useForm({ - defaultValues: initialValues ?? { target: DelegationTargetType.PassiveDelegation }, + defaultValues: initialValues ?? { type: DelegationTargetType.PassiveDelegation }, }); const submit = form.handleSubmit(onSubmit); - const target = form.watch('target'); + const target = form.watch('type'); const validateBakerId: Validate = async (value) => { try { @@ -53,7 +54,7 @@ export default function DelegationType({ initialValues, onSubmit }: Props) { return ( - + {t('description')} className="delegation-type__select-form" formMethods={form} onSubmit={onSubmit}> {(f) => ( @@ -61,13 +62,13 @@ export default function DelegationType({ initialValues, onSubmit }: Props) { {target === DelegationTargetType.Baker && ( @@ -108,7 +109,9 @@ export default function DelegationType({ initialValues, onSubmit }: Props) { /> )} - + + + ); } 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 b83a23787..c510c4550 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 @@ -37,8 +37,9 @@ const t = { body: 'When you make a delegation to either type of pool, your delegation amount will be locked on your account.\n\nThis means that you cannot use the amount for anything while it is still locked in for delegation.\n\nIf you decrease your delegation amount or stop the delegation altogether, the amount will still be locked for a cool-down period.\n\nAs transactions cost a fee, it is important to take into consideration that you will need some unlocked funds on your public balance to pay the fee for unlocking your delegation amount again.', }, }, + register: { title: 'Register delegation' }, + update: { title: 'Update delegation' }, target: { - title: 'Register delegation', description: 'You can delegate to an open pool of your choice, or you can stake using passive delegation.', radioValidatorLabel: 'Validator', radioPassiveLabel: 'Passive', @@ -55,6 +56,35 @@ const t = { 'Passive delegation is an alternative to delegation to a specific validator pool that has lower rewards. With passive delegation you do not have to worry about the uptime or quality of a baker node.\nFor more info you can visit <1>developer.concordium.software', buttonContinue: 'Continue', }, + stake: { + selectedAccount: 'on {{account}}', + token: { + label: 'Token', + value: 'CCD', + balance: '{{balance}} CCD available', + }, + inputAmount: { + label: 'Amount', + buttonMax: 'Stake max.', + }, + fee: { + label: 'Estimated transaction fee:', + value: '{{amount}} CCD', + }, + poolStake: { + label: 'Current pool', + value: '{{amount}} CCD', + }, + poolCap: { + label: 'Pool limit', + value: '{{amount}} CCD', + }, + redelegate: { + label: 'Restake rewards', + description: 'I want to automatically add my delegation rewards to my delegation amount.', + }, + buttonContinue: 'Continue', + }, }, validator: { intro: { diff --git a/packages/browser-wallet/src/popup/popupX/shared/Button/Button.scss b/packages/browser-wallet/src/popup/popupX/shared/Button/Button.scss index d94d7a3a5..4e1766293 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Button/Button.scss +++ b/packages/browser-wallet/src/popup/popupX/shared/Button/Button.scss @@ -7,7 +7,6 @@ cursor: pointer; width: 100%; padding: rem(16px) 0; - margin-top: auto; border-radius: rem(32px); background: $color-white; diff --git a/packages/browser-wallet/src/popup/popupX/shared/Button/Button.tsx b/packages/browser-wallet/src/popup/popupX/shared/Button/Button.tsx index e1a511ce3..f52c52630 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Button/Button.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/Button/Button.tsx @@ -27,7 +27,7 @@ export function ButtonBase({ function ButtonMain({ label, className, ...props }: { label: string } & ButtonProps) { return ( - + {label} ); diff --git a/packages/browser-wallet/src/popup/styles/util/_spacing.scss b/packages/browser-wallet/src/popup/styles/util/_spacing.scss index 41e4ccce9..d7c569cd5 100644 --- a/packages/browser-wallet/src/popup/styles/util/_spacing.scss +++ b/packages/browser-wallet/src/popup/styles/util/_spacing.scss @@ -48,6 +48,11 @@ p-h-50 = padding-left: 5rem; padding-right: 5rem; margin-#{$side}: #{rem($space)}; } } + .m-#{str-slice($side, 0, 1)}-neg-#{$space} { + @include with-first-last-mods { + margin-#{$side}: -#{rem($space)}; + } + } .p-#{str-slice($side, 0, 1)}-#{$space} { @include with-first-last-mods { padding-#{$side}: #{rem($space)}; From 567b6c3627d97845b790cbe3056b5a0107de015d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Tue, 22 Oct 2024 14:40:22 +0200 Subject: [PATCH 07/20] Connect delegation registration amount to account --- .../Delegator/Stake/DelegatorStake.tsx | 105 +++++++++--------- .../AccountInfoListenerContext.tsx | 22 ++-- 2 files changed, 63 insertions(+), 64 deletions(-) diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx index fc043f090..5323ec490 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx @@ -1,15 +1,17 @@ import React from 'react'; +import { UseFormReturn } from 'react-hook-form'; import Button from '@popup/popupX/shared/Button'; -import { ToggleCheckbox } from '@popup/popupX/shared/Form/ToggleCheckbox'; +import FormToggleCheckbox from '@popup/popupX/shared/Form/ToggleCheckbox'; import Page from '@popup/popupX/shared/Page'; import { useTranslation } from 'react-i18next'; -import { DelegationTargetType } from '@concordium/web-sdk'; -import { useForm } from '@popup/popupX/shared/Form'; +import { CcdAmount, DelegationTargetType } from '@concordium/web-sdk'; +import Form, { useForm } from '@popup/popupX/shared/Form'; +import TokenAmount, { AmountForm } from '@popup/popupX/shared/Form/TokenAmount'; +import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext/AccountInfoListenerContext'; +import { displayNameAndSplitAddress, useSelectedCredential } from '@popup/shared/utils/account-helpers'; /** The form values for delegator stake configuration step */ -export type DelegatorStakeForm = { - /** in CCD */ - amount: string; +export type DelegatorStakeForm = AmountForm & { /** Whether to add rewards to the stake or not */ redelegate: boolean; }; @@ -31,58 +33,61 @@ export default function DelegatorStake({ title, target, initialValues, onSubmit defaultValues: initialValues ?? { amount: '0.00', redelegate: true }, }); const submit = form.handleSubmit(onSubmit); + const selectedCred = useSelectedCredential(); + const selectedAccountInfo = useAccountInfo(selectedCred); + + if (selectedAccountInfo === undefined || selectedCred === undefined) { + return null; + } // FIXME: hardcoded... - const account = 'Account 1 / 6gk...k7o'; - const balance = '17,800.00'; - const amount = '12,600.00'; // TODO: is it even feasible to render it like this when it's input?? - const fee = '0,32'; + const accountShow = displayNameAndSplitAddress(selectedCred); + const fee = CcdAmount.fromCcd(0.032); const poolStake = '300,000.00'; const poolCap = '56,400.00'; return ( - {t('selectedAccount', { account })} -
-
- {t('token.label')} -
- {t('token.value')} - {t('token.balance', { balance })} -
-
-
- {t('inputAmount.label')} -
- {amount} - {t('inputAmount.buttonMax')} -
-
-
- {t('fee.label')} - {t('fee.value', { amount: fee })} -
-
- {target === DelegationTargetType.Baker && ( -
-
- {t('poolStake.label')} - {t('poolStake.value', { amount: poolStake })} -
-
- {t('poolCap.label')} - {t('poolCap.value', { amount: poolCap })} -
-
- )} -
-
- {t('redelegate.label')} - -
- {t('redelegate.description')} -
+ + {t('selectedAccount', { account: accountShow })} + +
+ {(f) => ( + <> + } + /> + {target === DelegationTargetType.Baker && ( +
+
+ {t('poolStake.label')} + + {t('poolStake.value', { amount: poolStake })} + +
+
+ {t('poolCap.label')} + + {t('poolCap.value', { amount: poolCap })} + +
+
+ )} +
+
+ {t('redelegate.label')} + +
+ {t('redelegate.description')} +
+ + )} + diff --git a/packages/browser-wallet/src/popup/shared/AccountInfoListenerContext/AccountInfoListenerContext.tsx b/packages/browser-wallet/src/popup/shared/AccountInfoListenerContext/AccountInfoListenerContext.tsx index 4cf76951a..5511c5168 100644 --- a/packages/browser-wallet/src/popup/shared/AccountInfoListenerContext/AccountInfoListenerContext.tsx +++ b/packages/browser-wallet/src/popup/shared/AccountInfoListenerContext/AccountInfoListenerContext.tsx @@ -1,5 +1,5 @@ import { useAtomValue, useSetAtom, useAtom, atom, WritableAtom } from 'jotai'; -import React, { createContext, ReactNode, useContext, useEffect, useMemo, useState } from 'react'; +import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react'; import { AccountAddress, AccountInfo } from '@concordium/web-sdk'; import { useTranslation } from 'react-i18next'; import i18next from 'i18next'; @@ -98,23 +98,22 @@ export default function AccountInfoListenerContextProvider({ children }: Props) * * N.B. has to be used inside an AccountInfoEmitterContext. */ -export function useAccountInfo(account: WalletCredential): AccountInfo | undefined { +export function useAccountInfo(account: WalletCredential | undefined): AccountInfo | undefined { const accountInfoEmitter = useContext(AccountInfoListenerContext); - const [accountInfo, refreshAccountInfo] = useAtom(accountInfoFamily(account.address)); + const [accountInfo, refreshAccountInfo] = useAtom(accountInfoFamily(account?.address ?? '')); const { genesisHash } = useAtomValue(networkConfigurationAtom); - const address = useMemo(() => account.address, [account]); useEffect(() => { - if (!accountInfo && account.status === CreationStatus.Confirmed) { + if (!accountInfo && account?.status === CreationStatus.Confirmed) { refreshAccountInfo(); } - }, [genesisHash, accountInfo, account.status]); + }, [genesisHash, accountInfo, account?.status]); useEffect(() => { - if (account.status === CreationStatus.Confirmed && accountInfoEmitter) { - accountInfoEmitter.subscribe(address); + if (account?.status === CreationStatus.Confirmed && accountInfoEmitter) { + accountInfoEmitter.subscribe(account.address); return () => { - accountInfoEmitter.unsubscribe(address); + accountInfoEmitter.unsubscribe(account.address); }; } return noOp; @@ -125,10 +124,5 @@ export function useAccountInfo(account: WalletCredential): AccountInfo | undefin export function useSelectedAccountInfo() { const cred = useSelectedCredential(); - - if (cred === undefined) { - return undefined; - } - return useAccountInfo(cred); } From ae82efa7f40d7a0221ba556ba4abe1bf639681a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Tue, 22 Oct 2024 15:48:31 +0200 Subject: [PATCH 08/20] Calculate transaction fee --- .../Delegator/Stake/DelegatorStake.tsx | 42 +++++++++++++++---- .../TransactionFlow/TransactionFlow.tsx | 24 ++++++++--- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx index 5323ec490..04377afe3 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx @@ -1,14 +1,25 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { UseFormReturn } from 'react-hook-form'; import Button from '@popup/popupX/shared/Button'; import FormToggleCheckbox from '@popup/popupX/shared/Form/ToggleCheckbox'; import Page from '@popup/popupX/shared/Page'; import { useTranslation } from 'react-i18next'; -import { CcdAmount, DelegationTargetType } from '@concordium/web-sdk'; +import { + CcdAmount, + DelegationTarget, + DelegationTargetType, + ConfigureDelegationPayload, + convertEnergyToMicroCcd, + Energy, +} from '@concordium/web-sdk'; import Form, { useForm } from '@popup/popupX/shared/Form'; import TokenAmount, { AmountForm } from '@popup/popupX/shared/Form/TokenAmount'; import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext/AccountInfoListenerContext'; import { displayNameAndSplitAddress, useSelectedCredential } from '@popup/shared/utils/account-helpers'; +import { getConfigureDelegationEnergyCost } from '@shared/utils/energy-helpers'; +import { parseTokenAmount } from '@popup/popupX/shared/utils/helpers'; +import { CCD_METADATA } from '@shared/constants/token-metadata'; +import { useBlockChainParameters } from '@popup/shared/BlockChainParametersProvider'; /** The form values for delegator stake configuration step */ export type DelegatorStakeForm = AmountForm & { @@ -19,10 +30,10 @@ export type DelegatorStakeForm = AmountForm & { type Props = { /** The title for the configuriation step */ title: string; - /** The delegation target type */ - target: DelegationTargetType; /** The initial values of the step, if any */ initialValues?: DelegatorStakeForm; + /** The delegation target */ + target: DelegationTarget; /** The submit handler triggered when submitting the form in the step */ onSubmit(values: DelegatorStakeForm): void; }; @@ -33,16 +44,31 @@ export default function DelegatorStake({ title, target, initialValues, onSubmit defaultValues: initialValues ?? { amount: '0.00', redelegate: true }, }); const submit = form.handleSubmit(onSubmit); + const chainParameters = useBlockChainParameters(); const selectedCred = useSelectedCredential(); const selectedAccountInfo = useAccountInfo(selectedCred); + const { amount, redelegate } = form.watch(); + const fee = useMemo(() => { + if (chainParameters === undefined) { + return undefined; + } - if (selectedAccountInfo === undefined || selectedCred === undefined) { + const payload: ConfigureDelegationPayload = { + stake: CcdAmount.fromMicroCcd(parseTokenAmount(amount, CCD_METADATA.decimals)), + delegationTarget: target, + restakeEarnings: redelegate, + }; + + const energy = getConfigureDelegationEnergyCost(payload); + return convertEnergyToMicroCcd(Energy.create(energy), chainParameters); + }, [target, amount, redelegate, selectedAccountInfo, chainParameters]); + + if (selectedAccountInfo === undefined || selectedCred === undefined || fee === undefined) { return null; } - // FIXME: hardcoded... const accountShow = displayNameAndSplitAddress(selectedCred); - const fee = CcdAmount.fromCcd(0.032); + // FIXME: hardcoded... const poolStake = '300,000.00'; const poolCap = '56,400.00'; @@ -62,7 +88,7 @@ export default function DelegatorStake({ title, target, initialValues, onSubmit buttonMaxLabel={t('inputAmount.buttonMax')} form={f as unknown as UseFormReturn} /> - {target === DelegationTargetType.Baker && ( + {target.delegateType === DelegationTargetType.Baker && (
{t('poolStake.label')} diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx index 7d4e92984..7d71478c0 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx @@ -1,7 +1,9 @@ -import MultiStepForm from '@popup/popupX/shared/MultiStepForm'; import React, { useCallback } from 'react'; -import { Location, useLocation, useNavigate } from 'react-router-dom'; +import { Location, Navigate, useLocation, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import { DelegationTarget, DelegationTargetType } from '@concordium/web-sdk'; + +import MultiStepForm from '@popup/popupX/shared/MultiStepForm'; import DelegatorStake, { DelegatorStakeForm } from '../Stake'; import DelegatorType, { DelegationTypeForm } from '../Type'; @@ -21,6 +23,7 @@ export default function TransactionFlow() { const handleDone = useCallback( (values: DelegatorForm) => { nav(pathname, { replace: true, state: values }); // Override current router entry with stateful version + // TODO: where do we go from here? }, [pathname] ); @@ -36,11 +39,22 @@ export default function TransactionFlow() { stake: { render: (initial, onNext, form) => { if (form.target === undefined) { - nav('..'); - return null; + return ; } - return ; + const target: DelegationTarget = + form.target.type === DelegationTargetType.PassiveDelegation + ? { delegateType: DelegationTargetType.PassiveDelegation } + : { delegateType: DelegationTargetType.Baker, bakerId: BigInt(form.target.bakerId!) }; + + return ( + + ); }, }, }} From 69098ffd90e8a4c9337607f92e84da53f24c18c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Wed, 23 Oct 2024 07:56:06 +0200 Subject: [PATCH 09/20] Add pool info to delegation amount view --- .../Delegator/Stake/DelegatorStake.tsx | 63 +++++++++++++------ 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx index 04377afe3..54e075f36 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx @@ -17,9 +17,52 @@ import TokenAmount, { AmountForm } from '@popup/popupX/shared/Form/TokenAmount'; import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext/AccountInfoListenerContext'; import { displayNameAndSplitAddress, useSelectedCredential } from '@popup/shared/utils/account-helpers'; import { getConfigureDelegationEnergyCost } from '@shared/utils/energy-helpers'; -import { parseTokenAmount } from '@popup/popupX/shared/utils/helpers'; +import { formatTokenAmount, parseTokenAmount } from '@popup/popupX/shared/utils/helpers'; import { CCD_METADATA } from '@shared/constants/token-metadata'; import { useBlockChainParameters } from '@popup/shared/BlockChainParametersProvider'; +import { useAtomValue } from 'jotai'; +import { grpcClientAtom } from '@popup/store/settings'; +import { useAsyncMemo } from 'wallet-common-helpers'; + +type PoolInfoProps = { + /** The validator pool ID to show information for */ + validatorId: bigint; +}; + +function PoolInfo({ validatorId }: PoolInfoProps) { + const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.stake' }); + const client = useAtomValue(grpcClientAtom); + const poolStatus = useAsyncMemo(async () => client.getPoolInfo(validatorId), undefined, []); + + if (poolStatus?.bakerEquityCapital === undefined) { + return null; + } + + const poolStake = poolStatus.bakerEquityCapital.microCcdAmount + poolStatus.delegatedCapital!.microCcdAmount; + + return ( +
+
+ {t('poolStake.label')} + + {t('poolStake.value', { amount: formatTokenAmount(poolStake, CCD_METADATA.decimals, 2) })} + +
+
+ {t('poolCap.label')} + + {t('poolCap.value', { + amount: formatTokenAmount( + poolStatus.delegatedCapitalCap!.microCcdAmount, + CCD_METADATA.decimals, + 2 + ), + })} + +
+
+ ); +} /** The form values for delegator stake configuration step */ export type DelegatorStakeForm = AmountForm & { @@ -68,9 +111,6 @@ export default function DelegatorStake({ title, target, initialValues, onSubmit } const accountShow = displayNameAndSplitAddress(selectedCred); - // FIXME: hardcoded... - const poolStake = '300,000.00'; - const poolCap = '56,400.00'; return ( @@ -89,20 +129,7 @@ export default function DelegatorStake({ title, target, initialValues, onSubmit form={f as unknown as UseFormReturn} /> {target.delegateType === DelegationTargetType.Baker && ( -
-
- {t('poolStake.label')} - - {t('poolStake.value', { amount: poolStake })} - -
-
- {t('poolCap.label')} - - {t('poolCap.value', { amount: poolCap })} - -
-
+ )}
From 378ddfb5da6a3f1396e75d5099356b509321d5a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Wed, 23 Oct 2024 10:21:24 +0200 Subject: [PATCH 10/20] Initial work on delegation result --- .../src/popup/popupX/constants/routes.ts | 4 + .../Delegator/Result/DelegationResult.tsx | 33 +++++++-- .../Delegator/Stake/DelegatorStake.tsx | 54 ++++---------- .../TransactionFlow/TransactionFlow.tsx | 12 ++- .../Delegator/Type/DelegationType.tsx | 11 +-- .../pages/EarningRewards/Delegator/util.ts | 73 +++++++++++++++++++ .../src/popup/popupX/shared/utils/helpers.ts | 12 ++- .../src/popup/popupX/shell/Routes.tsx | 5 ++ 8 files changed, 141 insertions(+), 63 deletions(-) create mode 100644 packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/util.ts diff --git a/packages/browser-wallet/src/popup/popupX/constants/routes.ts b/packages/browser-wallet/src/popup/popupX/constants/routes.ts index 16220cbf1..9a73b0cdc 100644 --- a/packages/browser-wallet/src/popup/popupX/constants/routes.ts +++ b/packages/browser-wallet/src/popup/popupX/constants/routes.ts @@ -176,6 +176,10 @@ export const relativeRoutes = { update: { path: 'update', }, + /** Submit configure delegator transaction */ + submit: { + path: 'submit', + }, }, }, }, diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx index e6819601b..4ff995cf6 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx @@ -1,13 +1,28 @@ import React from 'react'; +import { ConfigureDelegationPayload } from '@concordium/web-sdk'; +import { Navigate, useLocation, Location } from 'react-router-dom'; + import Button from '@popup/popupX/shared/Button'; +import Page from '@popup/popupX/shared/Page'; +import { formatCcdAmount } from '@popup/popupX/shared/utils/helpers'; +import { useGetConfigureDelegationCost } from '../util'; export default function DelegationResult() { + const { state } = useLocation() as Location & { + state: ConfigureDelegationPayload | undefined; + }; + const getCost = useGetConfigureDelegationCost(); + + if (state === undefined) { + return ; + } + + const fee = getCost(state); + + // FIXME: translations... return ( -
-
- Register Delegation - on Accout 1 / 6gk...k7o -
+ + This will lock your delegation amount. Amount is released after 14 days from the time you remove or decrease your delegation. @@ -19,7 +34,7 @@ export default function DelegationResult() {
Estimated transaction fee - 1,000.00 CCD + {fee !== undefined ? formatCcdAmount(fee) : '...'} CCD
Transaction hash @@ -28,7 +43,9 @@ export default function DelegationResult() {
- -
+ + + +
); } diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx index 54e075f36..cf7005182 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx @@ -4,25 +4,22 @@ import Button from '@popup/popupX/shared/Button'; import FormToggleCheckbox from '@popup/popupX/shared/Form/ToggleCheckbox'; import Page from '@popup/popupX/shared/Page'; import { useTranslation } from 'react-i18next'; -import { - CcdAmount, - DelegationTarget, - DelegationTargetType, - ConfigureDelegationPayload, - convertEnergyToMicroCcd, - Energy, -} from '@concordium/web-sdk'; +import { DelegationTargetType } from '@concordium/web-sdk'; import Form, { useForm } from '@popup/popupX/shared/Form'; import TokenAmount, { AmountForm } from '@popup/popupX/shared/Form/TokenAmount'; import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext/AccountInfoListenerContext'; import { displayNameAndSplitAddress, useSelectedCredential } from '@popup/shared/utils/account-helpers'; -import { getConfigureDelegationEnergyCost } from '@shared/utils/energy-helpers'; -import { formatTokenAmount, parseTokenAmount } from '@popup/popupX/shared/utils/helpers'; +import { formatTokenAmount } from '@popup/popupX/shared/utils/helpers'; import { CCD_METADATA } from '@shared/constants/token-metadata'; -import { useBlockChainParameters } from '@popup/shared/BlockChainParametersProvider'; import { useAtomValue } from 'jotai'; import { grpcClientAtom } from '@popup/store/settings'; import { useAsyncMemo } from 'wallet-common-helpers'; +import { + DelegationTypeForm, + DelegatorStakeForm, + configureDelegatorPayloadFromForm, + useGetConfigureDelegationCost, +} from '../util'; type PoolInfoProps = { /** The validator pool ID to show information for */ @@ -64,19 +61,12 @@ function PoolInfo({ validatorId }: PoolInfoProps) { ); } -/** The form values for delegator stake configuration step */ -export type DelegatorStakeForm = AmountForm & { - /** Whether to add rewards to the stake or not */ - redelegate: boolean; -}; - type Props = { /** The title for the configuriation step */ title: string; /** The initial values of the step, if any */ initialValues?: DelegatorStakeForm; - /** The delegation target */ - target: DelegationTarget; + target: DelegationTypeForm; /** The submit handler triggered when submitting the form in the step */ onSubmit(values: DelegatorStakeForm): void; }; @@ -87,36 +77,24 @@ export default function DelegatorStake({ title, target, initialValues, onSubmit defaultValues: initialValues ?? { amount: '0.00', redelegate: true }, }); const submit = form.handleSubmit(onSubmit); - const chainParameters = useBlockChainParameters(); const selectedCred = useSelectedCredential(); const selectedAccountInfo = useAccountInfo(selectedCred); const { amount, redelegate } = form.watch(); + const getCost = useGetConfigureDelegationCost(); const fee = useMemo(() => { - if (chainParameters === undefined) { - return undefined; - } - - const payload: ConfigureDelegationPayload = { - stake: CcdAmount.fromMicroCcd(parseTokenAmount(amount, CCD_METADATA.decimals)), - delegationTarget: target, - restakeEarnings: redelegate, - }; - - const energy = getConfigureDelegationEnergyCost(payload); - return convertEnergyToMicroCcd(Energy.create(energy), chainParameters); - }, [target, amount, redelegate, selectedAccountInfo, chainParameters]); + const payload = configureDelegatorPayloadFromForm({ target, stake: { amount, redelegate } }); + return getCost(payload); + }, [target, amount, redelegate, getCost]); if (selectedAccountInfo === undefined || selectedCred === undefined || fee === undefined) { return null; } - const accountShow = displayNameAndSplitAddress(selectedCred); - return ( - {t('selectedAccount', { account: accountShow })} + {t('selectedAccount', { account: displayNameAndSplitAddress(selectedCred) })}
{(f) => ( @@ -128,8 +106,8 @@ export default function DelegatorStake({ title, target, initialValues, onSubmit buttonMaxLabel={t('inputAmount.buttonMax')} form={f as unknown as UseFormReturn} /> - {target.delegateType === DelegationTargetType.Baker && ( - + {target.type === DelegationTargetType.Baker && ( + )}
diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx index 7d71478c0..72ba72c51 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx @@ -1,11 +1,12 @@ import React, { useCallback } from 'react'; import { Location, Navigate, useLocation, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { DelegationTarget, DelegationTargetType } from '@concordium/web-sdk'; import MultiStepForm from '@popup/popupX/shared/MultiStepForm'; +import { absoluteRoutes } from '@popup/popupX/constants/routes'; import DelegatorStake, { DelegatorStakeForm } from '../Stake'; import DelegatorType, { DelegationTypeForm } from '../Type'; +import { configureDelegatorPayloadFromForm } from '../util'; /** Represents the form data for a configure delegator transaction. */ type DelegatorForm = { @@ -22,8 +23,10 @@ export default function TransactionFlow() { const handleDone = useCallback( (values: DelegatorForm) => { + const payload = configureDelegatorPayloadFromForm(values); nav(pathname, { replace: true, state: values }); // Override current router entry with stateful version // TODO: where do we go from here? + nav(absoluteRoutes.settings.earn.delegator.submit.path, { state: payload }); // Override current router entry with stateful version }, [pathname] ); @@ -42,15 +45,10 @@ export default function TransactionFlow() { return ; } - const target: DelegationTarget = - form.target.type === DelegationTargetType.PassiveDelegation - ? { delegateType: DelegationTargetType.PassiveDelegation } - : { delegateType: DelegationTargetType.Baker, bakerId: BigInt(form.target.bakerId!) }; - return ( diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx index 5b75e005e..03a3f8cef 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/DelegationType.tsx @@ -10,14 +10,7 @@ import Button from '@popup/popupX/shared/Button'; import FormInput from '@popup/popupX/shared/Form/Input/Input'; import { useAtomValue } from 'jotai'; import { grpcClientAtom, networkConfigurationAtom } from '@popup/store/settings'; - -/** Describes the form values for configuring the delegation target of a delegation transaction */ -export type DelegationTypeForm = { - /** The target for the delegation */ - type: DelegationTargetType; - /** The target baker ID - only relevant for target = {@linkcode DelegationTargetType.Baker} */ - bakerId?: string; -}; +import { DelegationTypeForm } from '../util'; type Props = { title: string; @@ -72,7 +65,7 @@ export default function DelegationType({ initialValues, onSubmit, title }: Props register={f.register} /> {target === DelegationTargetType.Baker && ( - { + if (cp === undefined) { + return undefined; + } + + return getConfigureDelegationCost(payload, cp); + }; +} diff --git a/packages/browser-wallet/src/popup/popupX/shared/utils/helpers.ts b/packages/browser-wallet/src/popup/popupX/shared/utils/helpers.ts index 199a8f758..3d17971d1 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/utils/helpers.ts +++ b/packages/browser-wallet/src/popup/popupX/shared/utils/helpers.ts @@ -1,3 +1,6 @@ +import { CcdAmount } from '@concordium/web-sdk'; +import { CCD_METADATA } from '@shared/constants/token-metadata'; + export async function copyToClipboard(text: string): Promise { try { await navigator.clipboard.writeText(text); @@ -9,7 +12,7 @@ export async function copyToClipboard(text: string): Promise { export const removeNumberGrouping = (amount: string) => amount.replace(/,/g, ''); /** Display a token amount with a number of decimals + number groupings (thousand separators) */ -export function formatTokenAmount(amount: bigint, decimals = 0, minDecimals = decimals) { +export function formatTokenAmount(amount: bigint, decimals = 0, minDecimals = 2) { const padded = amount.toString().padStart(decimals + 1, '0'); // Ensure the string length is minimum decimals + 1 characters. For CCD, this would mean minimum 7 characters long const integer = padded.slice(0, -decimals); const fraction = padded.slice(-decimals); @@ -33,3 +36,10 @@ export function parseTokenAmount(amount: string, decimals = 0): bigint { const combined = integerPart + fractionPart; return BigInt(combined); } + +/** {@linkcode formatTokenAmount} for CCD + 2 minimum decimals */ +export const formatCcdAmount = (amount: CcdAmount.Type) => + formatTokenAmount(amount.microCcdAmount, CCD_METADATA.decimals); +/** {@linkcode parseTokenAmount} for CCD */ +export const parseCcdAmount = (amount: string): CcdAmount.Type => + CcdAmount.fromMicroCcd(parseTokenAmount(amount, CCD_METADATA.decimals)); diff --git a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx index f8bcd2ca4..586c29121 100644 --- a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx @@ -26,6 +26,7 @@ import { BakerKeys } from '@popup/popupX/pages/EarningRewards/Baker/BakerKeys'; import PrivateKey from '@popup/popupX/pages/PrivateKey'; import { RestoreIntro, RestoreResult } from '@popup/popupX/pages/Restore'; import RegisterDelegator from '../pages/EarningRewards/Delegator/RegisterDelegator'; +import { DelegationResult } from '../pages/EarningRewards/Delegator/Result'; export default function Routes() { return ( @@ -106,6 +107,10 @@ export default function Routes() { element={} // FIXME: change to update flow path={`${relativeRoutes.settings.earn.delegator.update.path}/*`} /> + } + path={relativeRoutes.settings.earn.delegator.submit.path} + /> From 43b1f4ff7085ba03f21c45fb99a2e93afa14d743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Wed, 23 Oct 2024 11:07:17 +0200 Subject: [PATCH 11/20] Register delegation submission layout --- .../Delegator/Result/DelegationResult.scss | 45 ----------- .../Delegator/Result/DelegationResult.tsx | 76 ++++++++++++++----- .../TransactionFlow/TransactionFlow.tsx | 7 +- .../src/popup/popupX/shared/Card/Card.tsx | 27 +++++-- 4 files changed, 80 insertions(+), 75 deletions(-) diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.scss b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.scss index 36ab1cc5d..1b6380508 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.scss +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.scss @@ -1,56 +1,11 @@ .delegation-result-container { - display: flex; - flex-direction: column; - height: 100%; - padding-bottom: rem(32px); - .capture__main_small { color: $color-white; word-wrap: break-word; } .delegation-result { - &__title { - display: flex; - flex-direction: column; - justify-content: space-between; - margin-bottom: rem(12px); - - .capture__main_small { - color: $color-mineral-2; - padding-left: rem(6px); - } - } - &__card { - display: flex; - flex-direction: column; - border-radius: rem(12px); - padding: rem(16px); - margin-top: rem(16px); - background-color: rgba($color-grey-3, 0.3); - - &_row:not(:last-child) { - padding-bottom: rem(8px); - margin-bottom: rem(8px); - border-bottom: 1px solid $color-grey-3; - } - - &_row { - display: flex; - flex-direction: column; - - .capture__main_small:first-child { - color: rgba($color-mineral-3, 0.5); - margin-bottom: rem(6px); - } - } - } - - &__export { - display: flex; - align-items: center; - gap: rem(8px); margin-top: rem(16px); } } diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx index 4ff995cf6..b462c1e67 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx @@ -1,23 +1,34 @@ import React from 'react'; -import { ConfigureDelegationPayload } from '@concordium/web-sdk'; +import { ConfigureDelegationPayload, DelegationTargetType } from '@concordium/web-sdk'; import { Navigate, useLocation, Location } from 'react-router-dom'; +import { useAtomValue } from 'jotai'; +import { selectedAccountAtom } from '@popup/store/account'; import Button from '@popup/popupX/shared/Button'; import Page from '@popup/popupX/shared/Page'; import { formatCcdAmount } from '@popup/popupX/shared/utils/helpers'; +import Card from '@popup/popupX/shared/Card'; +import { ensureDefined } from '@shared/utils/basic-helpers'; + import { useGetConfigureDelegationCost } from '../util'; +export type DelegationResultLocationState = { + payload: ConfigureDelegationPayload; + type: 'register' | 'change' | 'remove'; +}; + export default function DelegationResult() { const { state } = useLocation() as Location & { - state: ConfigureDelegationPayload | undefined; + state: DelegationResultLocationState | undefined; }; const getCost = useGetConfigureDelegationCost(); + const account = ensureDefined(useAtomValue(selectedAccountAtom), 'No account selected'); if (state === undefined) { return ; } - const fee = getCost(state); + const fee = getCost(state.payload); // FIXME: translations... return ( @@ -27,24 +38,49 @@ export default function DelegationResult() { This will lock your delegation amount. Amount is released after 14 days from the time you remove or decrease your delegation. -
-
- Transaction - Register delegation -
-
- Estimated transaction fee - {fee !== undefined ? formatCcdAmount(fee) : '...'} CCD -
-
- Transaction hash - - 4f84fg3gb6d9s9s3s1d46gg9grf7jmf9xc5c7s5x3vn80b8c6x5x4f84fg3gb6d9s9s3s1d4 - -
-
+ + + + + {state.payload.delegationTarget !== undefined && ( + + + + )} + {state.payload.stake !== undefined && ( + + + + )} + {state.payload.restakeEarnings !== undefined && ( + + + + )} + + + + - + ); diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx index 72ba72c51..98d30d484 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx @@ -7,6 +7,7 @@ import { absoluteRoutes } from '@popup/popupX/constants/routes'; import DelegatorStake, { DelegatorStakeForm } from '../Stake'; import DelegatorType, { DelegationTypeForm } from '../Type'; import { configureDelegatorPayloadFromForm } from '../util'; +import { DelegationResultLocationState } from '../Result/DelegationResult'; /** Represents the form data for a configure delegator transaction. */ type DelegatorForm = { @@ -24,9 +25,11 @@ export default function TransactionFlow() { const handleDone = useCallback( (values: DelegatorForm) => { const payload = configureDelegatorPayloadFromForm(values); + nav(pathname, { replace: true, state: values }); // Override current router entry with stateful version - // TODO: where do we go from here? - nav(absoluteRoutes.settings.earn.delegator.submit.path, { state: payload }); // Override current router entry with stateful version + + const submitDelegatorState: DelegationResultLocationState = { payload, type: 'register' }; + nav(absoluteRoutes.settings.earn.delegator.submit.path, { state: submitDelegatorState }); }, [pathname] ); diff --git a/packages/browser-wallet/src/popup/popupX/shared/Card/Card.tsx b/packages/browser-wallet/src/popup/popupX/shared/Card/Card.tsx index 2936b5880..ad01b33ff 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Card/Card.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/Card/Card.tsx @@ -4,23 +4,34 @@ import Text from '@popup/popupX/shared/Text'; type CardType = 'gradient' | 'transparent' | 'grey'; -function CardRoot({ - type = 'grey', - className, - children, -}: { +type CardRootProps = { type?: CardType; className?: string; children: ReactNode; -}) { +}; + +function CardRoot({ type = 'grey', className, children }: CardRootProps) { return
{children}
; } -function CardRow({ className, children }: { className?: string; children: ReactNode }) { +type CardRowProps = { + className?: string; + children: ReactNode; +}; + +function CardRow({ className, children }: CardRowProps) { return
{children}
; } -function CardRowDetails({ title, value, className }: { title?: string; value?: string; className?: string }) { +type CardRowDetailsProps = { + /** Title of the card row detail */ + title?: string; + /** Value of the card row detail */ + value?: string; + className?: string; +}; + +function CardRowDetails({ title, value, className }: CardRowDetailsProps) { return (
{title} From 534b24ae0a3286e81fcba6555edd2f9f3fea42f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Wed, 23 Oct 2024 11:28:30 +0200 Subject: [PATCH 12/20] Add translations --- .../Delegator/Result/DelegationResult.tsx | 68 ++++++++++++++----- .../popupX/pages/EarningRewards/i18n/en.ts | 17 ++++- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx index b462c1e67..3aae12fef 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx @@ -1,7 +1,8 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { ConfigureDelegationPayload, DelegationTargetType } from '@concordium/web-sdk'; import { Navigate, useLocation, Location } from 'react-router-dom'; import { useAtomValue } from 'jotai'; +import { useTranslation } from 'react-i18next'; import { selectedAccountAtom } from '@popup/store/account'; import Button from '@popup/popupX/shared/Button'; @@ -9,6 +10,8 @@ import Page from '@popup/popupX/shared/Page'; import { formatCcdAmount } from '@popup/popupX/shared/utils/helpers'; 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 { useGetConfigureDelegationCost } from '../util'; @@ -21,35 +24,66 @@ export default function DelegationResult() { const { state } = useLocation() as Location & { state: DelegationResultLocationState | undefined; }; + const { t } = useTranslation('x', { keyPrefix: 'earn.delegator' }); const getCost = useGetConfigureDelegationCost(); const account = ensureDefined(useAtomValue(selectedAccountAtom), 'No account selected'); + const parametersV1 = useBlockChainParametersAboveV0(); + + const cooldown = useMemo(() => { + let cooldownParam = 0n; + if (parametersV1 !== undefined) { + // From protocol version 7, the lower of the two values is the value that counts. + cooldownParam = + parametersV1.poolOwnerCooldown < parametersV1.delegatorCooldown + ? parametersV1.poolOwnerCooldown + : parametersV1.delegatorCooldown; + } + return secondsToDaysRoundedDown(cooldownParam); + }, [parametersV1]); + + const title = useMemo(() => { + switch (state?.type) { + case 'register': + return t('register.title'); + case 'change': + return t('update.title'); + // case 'remove': + // return t('remove.title'); + default: + return undefined; + } + }, [state, t]); + const notice = t('register.notice', { cooldown }); // TODO: add more cases when supporting change/remove + if (state === undefined) { return ; } const fee = getCost(state.payload); - // FIXME: translations... + const submit = () => { + console.log(state.payload); + }; + return ( - - - This will lock your delegation amount. Amount is released after 14 days from the time you remove or - decrease your delegation. - + + {notice} - + {state.payload.delegationTarget !== undefined && ( @@ -57,7 +91,7 @@ export default function DelegationResult() { {state.payload.stake !== undefined && ( @@ -65,22 +99,24 @@ export default function DelegationResult() { {state.payload.restakeEarnings !== undefined && ( )} - + ); 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 c510c4550..d54eec226 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 @@ -37,7 +37,10 @@ const t = { body: 'When you make a delegation to either type of pool, your delegation amount will be locked on your account.\n\nThis means that you cannot use the amount for anything while it is still locked in for delegation.\n\nIf you decrease your delegation amount or stop the delegation altogether, the amount will still be locked for a cool-down period.\n\nAs transactions cost a fee, it is important to take into consideration that you will need some unlocked funds on your public balance to pay the fee for unlocking your delegation amount again.', }, }, - register: { title: 'Register delegation' }, + register: { + title: 'Register delegation', + notice: 'This will lock your delegation amount. Amount is released after {{cooldown}} days from the time you remove or decrease your delegation.', + }, update: { title: 'Update delegation' }, target: { description: 'You can delegate to an open pool of your choice, or you can stake using passive delegation.', @@ -85,6 +88,18 @@ const t = { }, buttonContinue: 'Continue', }, + submit: { + sender: { label: 'Sender' }, + target: { label: 'Target', passive: 'Passive delegation', validator: 'Validator {{id}}' }, + amount: { label: 'Delegation amount' }, + redelegate: { + label: 'Rewards will be', + delegation: 'Added to delegation amount', + public: 'Added to public balance', + }, + fee: { label: 'Estimated transaction fee' }, + button: 'Submit delegation', + }, }, validator: { intro: { From df536fae2dca055e8445c8e5bf70a4524a99ae57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Wed, 23 Oct 2024 15:21:35 +0200 Subject: [PATCH 13/20] Submitted transaction page --- .../src/popup/popupX/constants/routes.ts | 13 ++ .../Delegator/Result/DelegationResult.tsx | 99 +++++++++-- .../Delegator/Stake/DelegatorStake.tsx | 20 +-- .../pages/EarningRewards/Delegator/util.ts | 26 +-- .../SubmittedTransaction.scss | 34 ++++ .../SubmittedTransaction.tsx | 154 ++++++++++++++++++ .../pages/SubmittedTransaction/index.ts | 1 + .../shared/utils/transaction-helpers.ts | 22 +++ .../src/popup/popupX/shell/Routes.tsx | 5 + .../shared/utils/chain-parameters-helpers.ts | 8 +- 10 files changed, 333 insertions(+), 49 deletions(-) create mode 100644 packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.scss create mode 100644 packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx create mode 100644 packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/index.ts diff --git a/packages/browser-wallet/src/popup/popupX/constants/routes.ts b/packages/browser-wallet/src/popup/popupX/constants/routes.ts index 9a73b0cdc..d22b073ab 100644 --- a/packages/browser-wallet/src/popup/popupX/constants/routes.ts +++ b/packages/browser-wallet/src/popup/popupX/constants/routes.ts @@ -1,3 +1,5 @@ +import { AccountAddress, HexString, TransactionHash } from '@concordium/web-sdk'; + export type RouteConfig = { hideBackArrow?: boolean; backTitle?: string; @@ -92,6 +94,9 @@ export const relativeRoutes = { token: { path: 'token', }, + submittedTransaction: { + path: ':transactionHash', + }, }, settings: { path: 'settings', @@ -216,6 +221,14 @@ const buildAbsoluteRoutes = ( export const absoluteRoutes = buildAbsoluteRoutes(relativeRoutes); +export const transactionDetailsRoute = (account: AccountAddress.Type, tx: TransactionHash.Type) => + absoluteRoutes.home.transactionLog.details.path + .replace(':account', account.address) + .replace(':transactionHash', TransactionHash.toHexString(tx)); + +export const submittedTransactionRoute = (tx: TransactionHash.Type) => + absoluteRoutes.home.submittedTransaction.path.replace(':transactionHash', TransactionHash.toHexString(tx)); + /** * Given two absolute routes, returns the relative route between them. * Note: fromPath should be a prefix of toPath. diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx index 3aae12fef..13940a4d4 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx @@ -1,6 +1,14 @@ -import React, { useMemo } from 'react'; -import { ConfigureDelegationPayload, DelegationTargetType } from '@concordium/web-sdk'; -import { Navigate, useLocation, Location } from 'react-router-dom'; +import React, { useCallback, useMemo } from 'react'; +import { + AccountAddress, + AccountTransactionPayload, + AccountTransactionType, + CcdAmount, + ConfigureDelegationPayload, + DelegationTargetType, + TransactionHash, +} from '@concordium/web-sdk'; +import { Navigate, useLocation, Location, useNavigate } from 'react-router-dom'; import { useAtomValue } from 'jotai'; import { useTranslation } from 'react-i18next'; @@ -13,7 +21,68 @@ import { ensureDefined } from '@shared/utils/basic-helpers'; import { useBlockChainParametersAboveV0 } from '@popup/shared/BlockChainParametersProvider'; import { secondsToDaysRoundedDown } from '@shared/utils/time-helpers'; -import { useGetConfigureDelegationCost } from '../util'; +import { grpcClientAtom } from '@popup/store/settings'; +import { usePrivateKey } from '@popup/shared/utils/account-helpers'; +import { + createPendingTransactionFromAccountTransaction, + getDefaultExpiry, + getTransactionAmount, + sendTransaction, +} from '@popup/shared/utils/transaction-helpers'; +import { useUpdateAtom } from 'jotai/utils'; +import { addPendingTransactionAtom } from '@popup/store/transactions'; +import { cpStakingCooldown } from '@shared/utils/chain-parameters-helpers'; +import { useGetTransactionFee } from '@popup/popupX/shared/utils/transaction-helpers'; +import { submittedTransactionRoute } from '@popup/popupX/constants/routes'; + +enum TransactionSubmitErrorType { + InsufficientFunds = 'InsufficientFunds', +} + +class TransactionSubmitError extends Error { + private constructor(public type: TransactionSubmitErrorType) { + super(); + super.name = `${'TransactionSubmitError'}.type`; + } + + public static insufficientFunds(): TransactionSubmitError { + return new TransactionSubmitError(TransactionSubmitErrorType.InsufficientFunds); + } +} + +function useTransactionSubmit(sender: AccountAddress.Type, type: AccountTransactionType) { + const grpc = useAtomValue(grpcClientAtom); + const key = usePrivateKey(sender.address); + const addPendingTransaction = useUpdateAtom(addPendingTransactionAtom); + + return useCallback( + async (payload: AccountTransactionPayload, cost: CcdAmount.Type) => { + const accountInfo = await grpc.getAccountInfo(sender); + if ( + accountInfo.accountAvailableBalance.microCcdAmount < + getTransactionAmount(type, payload) + (cost.microCcdAmount || 0n) + ) { + throw TransactionSubmitError.insufficientFunds(); + } + + const nonce = await grpc.getNextAccountNonce(sender); + + const header = { + expiry: getDefaultExpiry(), + sender, + nonce: nonce.nonce, + }; + const transaction = { payload, header, type }; + + const hash = await sendTransaction(grpc, transaction, key!); + const pending = createPendingTransactionFromAccountTransaction(transaction, hash, cost.microCcdAmount); + await addPendingTransaction(pending); + + return hash; + }, + [key] + ); +} export type DelegationResultLocationState = { payload: ConfigureDelegationPayload; @@ -24,20 +93,21 @@ export default function DelegationResult() { const { state } = useLocation() as Location & { state: DelegationResultLocationState | undefined; }; + const nav = useNavigate(); const { t } = useTranslation('x', { keyPrefix: 'earn.delegator' }); - const getCost = useGetConfigureDelegationCost(); + const getCost = useGetTransactionFee(AccountTransactionType.ConfigureDelegation); const account = ensureDefined(useAtomValue(selectedAccountAtom), 'No account selected'); const parametersV1 = useBlockChainParametersAboveV0(); + const submitTransaction = useTransactionSubmit( + AccountAddress.fromBase58(account), + AccountTransactionType.ConfigureDelegation + ); const cooldown = useMemo(() => { let cooldownParam = 0n; if (parametersV1 !== undefined) { - // From protocol version 7, the lower of the two values is the value that counts. - cooldownParam = - parametersV1.poolOwnerCooldown < parametersV1.delegatorCooldown - ? parametersV1.poolOwnerCooldown - : parametersV1.delegatorCooldown; + cooldownParam = cpStakingCooldown(parametersV1); } return secondsToDaysRoundedDown(cooldownParam); }, [parametersV1]); @@ -61,9 +131,12 @@ export default function DelegationResult() { } const fee = getCost(state.payload); - - const submit = () => { - console.log(state.payload); + const submit = async () => { + if (fee === undefined) { + throw Error('Fee could not be calculated'); + } + const tx = await submitTransaction(state.payload, fee); + nav(submittedTransactionRoute(TransactionHash.fromHexString(tx))); }; return ( diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx index cf7005182..2740af188 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx @@ -1,25 +1,23 @@ import React, { useMemo } from 'react'; import { UseFormReturn } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useAtomValue } from 'jotai'; +import { useAsyncMemo } from 'wallet-common-helpers'; +import { AccountTransactionType, DelegationTargetType } from '@concordium/web-sdk'; + import Button from '@popup/popupX/shared/Button'; import FormToggleCheckbox from '@popup/popupX/shared/Form/ToggleCheckbox'; import Page from '@popup/popupX/shared/Page'; -import { useTranslation } from 'react-i18next'; -import { DelegationTargetType } from '@concordium/web-sdk'; import Form, { useForm } from '@popup/popupX/shared/Form'; import TokenAmount, { AmountForm } from '@popup/popupX/shared/Form/TokenAmount'; import { useAccountInfo } from '@popup/shared/AccountInfoListenerContext/AccountInfoListenerContext'; import { displayNameAndSplitAddress, useSelectedCredential } from '@popup/shared/utils/account-helpers'; import { formatTokenAmount } from '@popup/popupX/shared/utils/helpers'; import { CCD_METADATA } from '@shared/constants/token-metadata'; -import { useAtomValue } from 'jotai'; import { grpcClientAtom } from '@popup/store/settings'; -import { useAsyncMemo } from 'wallet-common-helpers'; -import { - DelegationTypeForm, - DelegatorStakeForm, - configureDelegatorPayloadFromForm, - useGetConfigureDelegationCost, -} from '../util'; +import { useGetTransactionFee } from '@popup/popupX/shared/utils/transaction-helpers'; + +import { DelegationTypeForm, DelegatorStakeForm, configureDelegatorPayloadFromForm } from '../util'; type PoolInfoProps = { /** The validator pool ID to show information for */ @@ -80,7 +78,7 @@ export default function DelegatorStake({ title, target, initialValues, onSubmit const selectedCred = useSelectedCredential(); const selectedAccountInfo = useAccountInfo(selectedCred); const { amount, redelegate } = form.watch(); - const getCost = useGetConfigureDelegationCost(); + const getCost = useGetTransactionFee(AccountTransactionType.ConfigureDelegation); const fee = useMemo(() => { const payload = configureDelegatorPayloadFromForm({ target, stake: { amount, redelegate } }); return getCost(payload); diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/util.ts b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/util.ts index d98cc4382..fe9a76ab1 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/util.ts +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/util.ts @@ -1,16 +1,14 @@ import { - CcdAmount, - ChainParameters, + AccountTransactionType, ConfigureDelegationPayload, DelegationTarget, DelegationTargetType, - Energy, convertEnergyToMicroCcd, + getEnergyCost, } from '@concordium/web-sdk'; import { AmountForm } from '@popup/popupX/shared/Form/TokenAmount'; import { parseCcdAmount } from '@popup/popupX/shared/utils/helpers'; import { useBlockChainParameters } from '@popup/shared/BlockChainParametersProvider'; -import { getConfigureDelegationEnergyCost } from '@shared/utils/energy-helpers'; /** Describes the form values for configuring the delegation target of a delegation transaction */ export type DelegationTypeForm = { @@ -51,23 +49,3 @@ export function configureDelegatorPayloadFromForm(values: DelegatorForm): Config delegationTarget, }; } - -function getConfigureDelegationCost( - payload: ConfigureDelegationPayload, - chainParameters: ChainParameters -): CcdAmount.Type { - const energy = getConfigureDelegationEnergyCost(payload); - return convertEnergyToMicroCcd(Energy.create(energy), chainParameters); -} - -export function useGetConfigureDelegationCost() { - const cp = useBlockChainParameters(); - - return (payload: ConfigureDelegationPayload) => { - if (cp === undefined) { - return undefined; - } - - return getConfigureDelegationCost(payload, cp); - }; -} diff --git a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.scss b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.scss new file mode 100644 index 000000000..ce5ad40d7 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.scss @@ -0,0 +1,34 @@ +.submitted-tx { + &__card { + display: flex; + flex-direction: column; + align-items: center; + margin-top: rem(16px); + padding: rem(24px) 0 rem(36px) 0; + border: 1px solid rgba($color-grey-4, 0.4); + border-radius: rem(12px); + + .capture__main_small { + color: $color-white; + } + + .heading_large { + margin: rem(8px) 0; + } + + svg { + margin-bottom: rem(30px); + } + } + + &__details { + display: flex; + justify-content: center; + margin-top: rem(24px); + + .label__regular { + color: $color-white; + margin-right: rem(8px); + } + } +} diff --git a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx new file mode 100644 index 000000000..8a5b292b9 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx @@ -0,0 +1,154 @@ +import React, { useMemo } from 'react'; +import { Location, Navigate, useLocation, useNavigate, useParams } from 'react-router-dom'; + +import CheckCircle from '@assets/svgX/check-circle.svg'; +import Arrow from '@assets/svgX/arrow-right.svg'; +import Button from '@popup/popupX/shared/Button'; +import Page from '@popup/popupX/shared/Page'; +import { absoluteRoutes, transactionDetailsRoute } from '@popup/popupX/constants/routes'; +import Card from '@popup/popupX/shared/Card'; +import { useAsyncMemo } from 'wallet-common-helpers'; +import { + AccountTransactionSummary, + HexString, + TransactionHash, + TransactionSummaryType, + isRejectTransaction, + isSuccessTransaction, + TransactionKindString, + FailedTransactionSummary, + BaseAccountTransactionSummary, +} from '@concordium/web-sdk'; +import { useAtomValue } from 'jotai'; +import { grpcClientAtom } from '@popup/store/settings'; + +const TX_TIMEOUT = 60 * 1000; // 1 minute + +type SuccessSummary = Exclude; +type FailureSummary = BaseAccountTransactionSummary & FailedTransactionSummary; + +type Status = + | { type: 'success'; summary: SuccessSummary } + | { type: 'failure'; summary: FailureSummary } + | { type: 'error'; message: string }; + +type SuccessProps = { + tx: SuccessSummary; +}; + +// TODO: +// 1. Handle delegation transaction case +function Success({ tx }: SuccessProps) { + const body = useMemo(() => { + switch (tx.transactionType) { + case TransactionKindString.Transfer: { + return ( + <> + You’ve sent + 12,600.00 + CCD + + ); + } + default: + throw new Error(`${tx.transactionType} transactions are not supported`); + } + }, [tx]); + + return ( + <> + + {body} + + ); +} +type FailureProps = { + message: string; +}; + +// TODO: +// 1. Proper error icon +function Failure({ message }: FailureProps) { + return ( + <> + + {message} + + ); +} + +export type SubmittedTransactionParams = { + /** The transaction to show the status for */ + txHash: HexString; +}; + +export default function SubmittedTransaction() { + const { txHash } = useParams(); + const nav = useNavigate(); + const grpc = useAtomValue(grpcClientAtom); + + const status = useAsyncMemo( + async (): Promise => { + if (txHash === undefined) { + throw new Error('Transaction not specified in url'); + } + + try { + const outcome = await grpc.waitForTransactionFinalization( + TransactionHash.fromHexString(txHash), + TX_TIMEOUT + ); + + if (isRejectTransaction(outcome.summary)) { + return { type: 'failure', summary: outcome.summary }; + } + if ( + isSuccessTransaction(outcome.summary) && + outcome.summary.type === TransactionSummaryType.AccountTransaction + ) { + return { type: 'success', summary: outcome.summary }; + } + + throw Error('Unexpected transaction type'); + } catch (e) { + return { type: 'error', message: (e as Error).message ?? `${e}` }; + } + }, + undefined, + [txHash, grpc] + ); + + if (txHash === undefined) { + return ; + } + + // FIXME: + // 1. translations... + // 2. finalizing state (undefined) + return ( + + + {status?.type === 'success' && } + {status?.type === 'failure' && ( + + )} + {status?.type === 'error' && } + + {status?.type !== undefined && status.type !== 'error' && ( + } + label="Transaction details" + leftLabel + onClick={() => nav(transactionDetailsRoute(status.summary.sender, status.summary.hash))} + /> + )} + + nav(absoluteRoutes.home.path)} + label="Return to Account" + /> + + + ); +} diff --git a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/index.ts b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/index.ts new file mode 100644 index 000000000..d21a93a70 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/index.ts @@ -0,0 +1 @@ +export { default } from './SubmittedTransaction'; diff --git a/packages/browser-wallet/src/popup/popupX/shared/utils/transaction-helpers.ts b/packages/browser-wallet/src/popup/popupX/shared/utils/transaction-helpers.ts index fbb2662dd..af6eeb720 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/utils/transaction-helpers.ts +++ b/packages/browser-wallet/src/popup/popupX/shared/utils/transaction-helpers.ts @@ -2,11 +2,17 @@ import { AccountAddress, AccountInfo, AccountInfoType, + AccountTransactionType, BakerPoolStatusDetails, ChainParameters, ChainParametersV0, + ConfigureDelegationPayload, + convertEnergyToMicroCcd, + getEnergyCost, } from '@concordium/web-sdk'; +import { useBlockChainParameters } from '@popup/shared/BlockChainParametersProvider'; import i18n from '@popup/shell/i18n'; +import { useCallback } from 'react'; import { ccdToMicroCcd, displayAsCcd, @@ -117,3 +123,19 @@ export function validateDelegationAmount( return undefined; } + +/** Hook which exposes a function for getting the transaction fee for a given transaction type */ +export function useGetTransactionFee(type: AccountTransactionType) { + const cp = useBlockChainParameters(); + + return useCallback( + (payload: ConfigureDelegationPayload) => { + if (cp === undefined) { + return undefined; + } + const energy = getEnergyCost(type, payload); + return convertEnergyToMicroCcd(energy, cp); + }, + [cp, type] + ); +} diff --git a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx index 586c29121..4507985c9 100644 --- a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx @@ -27,6 +27,7 @@ import PrivateKey from '@popup/popupX/pages/PrivateKey'; import { RestoreIntro, RestoreResult } from '@popup/popupX/pages/Restore'; import RegisterDelegator from '../pages/EarningRewards/Delegator/RegisterDelegator'; import { DelegationResult } from '../pages/EarningRewards/Delegator/Result'; +import SubmittedTransaction from '../pages/SubmittedTransaction'; export default function Routes() { return ( @@ -61,6 +62,10 @@ export default function Routes() { } path={relativeRoutes.home.token.path} /> } path={`${relativeRoutes.home.token.path}/ccd`} /> + } + path={`${relativeRoutes.home.submittedTransaction.path}`} + /> } path={relativeRoutes.settings.path}> } path={relativeRoutes.settings.idCards.path} /> diff --git a/packages/browser-wallet/src/shared/utils/chain-parameters-helpers.ts b/packages/browser-wallet/src/shared/utils/chain-parameters-helpers.ts index c56a09206..b7551859a 100644 --- a/packages/browser-wallet/src/shared/utils/chain-parameters-helpers.ts +++ b/packages/browser-wallet/src/shared/utils/chain-parameters-helpers.ts @@ -1,4 +1,4 @@ -import { CcdAmount, ChainParameters } from '@concordium/web-sdk/types'; +import { CcdAmount, ChainParameters, ChainParametersV0 } from '@concordium/web-sdk/types'; export function cpBakingThreshold(cp: ChainParameters): CcdAmount.Type { switch (cp.version) { @@ -13,3 +13,9 @@ export function cpBakingThreshold(cp: ChainParameters): CcdAmount.Type { throw new Error('Non-supported chain parameters version'); } } + +/** Get the staking cooldown from the chain parameters. */ +export function cpStakingCooldown(cp: Exclude): bigint { + // From protocol version 7, the lower of the two values is the value that counts. + return cp.poolOwnerCooldown < cp.delegatorCooldown ? cp.poolOwnerCooldown : cp.delegatorCooldown; +} From 4fa9cef8cc69f0c96ae06d68ac2e0cb3a2da3a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Thu, 24 Oct 2024 10:24:04 +0200 Subject: [PATCH 14/20] Cleanup --- packages/browser-wallet/package.json | 1 + .../src/popup/popupX/constants/routes.ts | 2 +- .../pages/EarningRewards/Delegator/Stake/index.ts | 2 +- .../Delegator/TransactionFlow/TransactionFlow.tsx | 14 +++----------- .../pages/EarningRewards/Delegator/Type/index.ts | 2 +- .../popupX/pages/EarningRewards/Delegator/util.ts | 10 +--------- .../SubmittedTransaction/SubmittedTransaction.tsx | 15 +++++++++++++-- packages/browser-wallet/src/popup/shell/Root.tsx | 4 ++-- 8 files changed, 23 insertions(+), 27 deletions(-) diff --git a/packages/browser-wallet/package.json b/packages/browser-wallet/package.json index 34a1ac9b9..c4724a27d 100644 --- a/packages/browser-wallet/package.json +++ b/packages/browser-wallet/package.json @@ -10,6 +10,7 @@ "build:dev": "cross-env NODE_ENV=development yarn build-base", "build:prod": "cross-env NODE_ENV=production yarn build-base", "build": "yarn build:prod", + "check": "tsc --noEmit", "test": "jest", "watch": "cross-env WATCH=1 yarn build:dev", "storybook": "storybook dev -p 6006", diff --git a/packages/browser-wallet/src/popup/popupX/constants/routes.ts b/packages/browser-wallet/src/popup/popupX/constants/routes.ts index d22b073ab..db6e07522 100644 --- a/packages/browser-wallet/src/popup/popupX/constants/routes.ts +++ b/packages/browser-wallet/src/popup/popupX/constants/routes.ts @@ -1,4 +1,4 @@ -import { AccountAddress, HexString, TransactionHash } from '@concordium/web-sdk'; +import { AccountAddress, TransactionHash } from '@concordium/web-sdk'; export type RouteConfig = { hideBackArrow?: boolean; diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/index.ts b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/index.ts index 803d25b73..e39a2bee3 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/index.ts +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/index.ts @@ -1 +1 @@ -export { default, type DelegatorStakeForm } from './DelegatorStake'; +export { default } from './DelegatorStake'; diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx index 98d30d484..fc9ad4870 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx @@ -4,19 +4,11 @@ import { useTranslation } from 'react-i18next'; import MultiStepForm from '@popup/popupX/shared/MultiStepForm'; import { absoluteRoutes } from '@popup/popupX/constants/routes'; -import DelegatorStake, { DelegatorStakeForm } from '../Stake'; -import DelegatorType, { DelegationTypeForm } from '../Type'; -import { configureDelegatorPayloadFromForm } from '../util'; +import DelegatorStake from '../Stake'; +import DelegatorType from '../Type'; +import { configureDelegatorPayloadFromForm, type DelegatorForm } from '../util'; import { DelegationResultLocationState } from '../Result/DelegationResult'; -/** Represents the form data for a configure delegator transaction. */ -type DelegatorForm = { - /** The delegation target configuration */ - target: DelegationTypeForm; - /** The delegation stake configuration */ - stake: DelegatorStakeForm; -}; - export default function TransactionFlow() { const { state: initialValues, pathname } = useLocation() as Location & { state: DelegatorForm | undefined }; const nav = useNavigate(); diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/index.ts b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/index.ts index c801c748c..49f1466c9 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/index.ts +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Type/index.ts @@ -1 +1 @@ -export { default, type DelegationTypeForm } from './DelegationType'; +export { default } from './DelegationType'; diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/util.ts b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/util.ts index fe9a76ab1..ae6f84043 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/util.ts +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/util.ts @@ -1,14 +1,6 @@ -import { - AccountTransactionType, - ConfigureDelegationPayload, - DelegationTarget, - DelegationTargetType, - convertEnergyToMicroCcd, - getEnergyCost, -} from '@concordium/web-sdk'; +import { ConfigureDelegationPayload, DelegationTarget, DelegationTargetType } from '@concordium/web-sdk'; import { AmountForm } from '@popup/popupX/shared/Form/TokenAmount'; import { parseCcdAmount } from '@popup/popupX/shared/utils/helpers'; -import { useBlockChainParameters } from '@popup/shared/BlockChainParametersProvider'; /** Describes the form values for configuring the delegation target of a delegation transaction */ export type DelegationTypeForm = { diff --git a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx index 8a5b292b9..9d30725bb 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react'; -import { Location, Navigate, useLocation, useNavigate, useParams } from 'react-router-dom'; +import { Navigate, useNavigate, useParams } from 'react-router-dom'; import CheckCircle from '@assets/svgX/check-circle.svg'; import Arrow from '@assets/svgX/arrow-right.svg'; @@ -77,6 +77,17 @@ function Failure({ message }: FailureProps) { ); } +// TODO: +// 1. Proper error icon +function Finalizing() { + return ( + <> + + Finalizing on chain + + ); +} + export type SubmittedTransactionParams = { /** The transaction to show the status for */ txHash: HexString; @@ -124,10 +135,10 @@ export default function SubmittedTransaction() { // FIXME: // 1. translations... - // 2. finalizing state (undefined) return ( + {status === undefined && } {status?.type === 'success' && } {status?.type === 'failure' && ( diff --git a/packages/browser-wallet/src/popup/shell/Root.tsx b/packages/browser-wallet/src/popup/shell/Root.tsx index 5297ac295..d9b92d1df 100644 --- a/packages/browser-wallet/src/popup/shell/Root.tsx +++ b/packages/browser-wallet/src/popup/shell/Root.tsx @@ -15,7 +15,7 @@ import AccountInfoListenerContext from '@popup/shared/AccountInfoListenerContext import './i18n'; import { mainnet } from '@shared/constants/networkConfiguration'; -import { routePrefix } from '@popup/popupX/constants/routes'; +import { routePrefix, absoluteRoutes } from '@popup/popupX/constants/routes'; import Routes from './Routes'; import RoutesX from '../popupX/shell/Routes'; @@ -123,7 +123,7 @@ function Theme({ children }: { children: ReactElement }) { export default function Root() { return ( - + From 548477d618b63ff55747739673f86061ed569ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Thu, 24 Oct 2024 11:23:06 +0200 Subject: [PATCH 15/20] Handle configure delegation in submitted view --- .../src/popup/popupX/constants/routes.ts | 2 +- .../Delegator/Stake/DelegatorStake.tsx | 1 + .../SubmittedTransaction.scss | 17 +--- .../SubmittedTransaction.tsx | 77 ++++++++++++------- .../shared/Form/TokenAmount/TokenAmount.tsx | 19 +++-- .../src/popup/popupX/styles/_elements.scss | 1 + .../browser-wallet/src/popup/shell/Root.tsx | 6 +- 7 files changed, 76 insertions(+), 47 deletions(-) diff --git a/packages/browser-wallet/src/popup/popupX/constants/routes.ts b/packages/browser-wallet/src/popup/popupX/constants/routes.ts index c01cfc782..354ff2d06 100644 --- a/packages/browser-wallet/src/popup/popupX/constants/routes.ts +++ b/packages/browser-wallet/src/popup/popupX/constants/routes.ts @@ -95,7 +95,7 @@ export const relativeRoutes = { path: 'token', }, submittedTransaction: { - path: ':transactionHash', + path: 'submitted/:transactionHash', }, }, settings: { diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx index 2740af188..da5a798b9 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx @@ -103,6 +103,7 @@ export default function DelegatorStake({ title, target, initialValues, onSubmit tokenType="ccd" buttonMaxLabel={t('inputAmount.buttonMax')} form={f as unknown as UseFormReturn} + ccdBalance="total" /> {target.type === DelegationTargetType.Baker && ( diff --git a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.scss b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.scss index ce5ad40d7..7358d59ed 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.scss +++ b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.scss @@ -1,12 +1,8 @@ .submitted-tx { + padding-top: rem(50px); &__card { - display: flex; - flex-direction: column; align-items: center; - margin-top: rem(16px); - padding: rem(24px) 0 rem(36px) 0; - border: 1px solid rgba($color-grey-4, 0.4); - border-radius: rem(12px); + padding: rem(24px) !important; .capture__main_small { color: $color-white; @@ -21,14 +17,7 @@ } } - &__details { - display: flex; - justify-content: center; + &__details-btn { margin-top: rem(24px); - - .label__regular { - color: $color-white; - margin-right: rem(8px); - } } } diff --git a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx index 9d30725bb..866cf43de 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { Navigate, useNavigate, useParams } from 'react-router-dom'; import CheckCircle from '@assets/svgX/check-circle.svg'; @@ -18,12 +18,45 @@ import { TransactionKindString, FailedTransactionSummary, BaseAccountTransactionSummary, + TransactionEventTag, + DelegationStakeChangedEvent, + DelegatorEvent, + ConfigureDelegationSummary, } from '@concordium/web-sdk'; import { useAtomValue } from 'jotai'; import { grpcClientAtom } from '@popup/store/settings'; +import { formatCcdAmount } from '@popup/popupX/shared/utils/helpers'; const TX_TIMEOUT = 60 * 1000; // 1 minute +type DelegationBodyProps = BaseAccountTransactionSummary & ConfigureDelegationSummary; + +function DelegationBody({ events }: DelegationBodyProps) { + const stakeChange = events.find((e) => + [TransactionEventTag.DelegationStakeIncreased, TransactionEventTag.DelegationStakeDecreased].includes(e.tag) + ) as DelegationStakeChangedEvent | undefined; + + if (stakeChange !== undefined) { + return ( + <> + You’ve delegated + {formatCcdAmount(stakeChange.newStake)} + CCD + + ); + } + + const removal = events.find((e) => [TransactionEventTag.DelegationRemoved].includes(e.tag)) as + | DelegatorEvent + | undefined; + + if (removal !== undefined) { + return You’ve removed your delegated stake; + } + + return You’ve updated your delegation settings; +} + type SuccessSummary = Exclude; type FailureSummary = BaseAccountTransactionSummary & FailedTransactionSummary; @@ -39,26 +72,17 @@ type SuccessProps = { // TODO: // 1. Handle delegation transaction case function Success({ tx }: SuccessProps) { - const body = useMemo(() => { - switch (tx.transactionType) { - case TransactionKindString.Transfer: { - return ( - <> - You’ve sent - 12,600.00 - CCD - - ); - } - default: - throw new Error(`${tx.transactionType} transactions are not supported`); - } - }, [tx]); - return ( <> - {body} + {tx.transactionType === TransactionKindString.Transfer && ( + <> + You’ve sent + 12,600.00 + CCD + + )} + {tx.transactionType === TransactionKindString.ConfigureDelegation && } ); } @@ -90,23 +114,23 @@ function Finalizing() { export type SubmittedTransactionParams = { /** The transaction to show the status for */ - txHash: HexString; + transactionHash: HexString; }; export default function SubmittedTransaction() { - const { txHash } = useParams(); + const { transactionHash } = useParams(); const nav = useNavigate(); const grpc = useAtomValue(grpcClientAtom); const status = useAsyncMemo( async (): Promise => { - if (txHash === undefined) { + if (transactionHash === undefined) { throw new Error('Transaction not specified in url'); } try { const outcome = await grpc.waitForTransactionFinalization( - TransactionHash.fromHexString(txHash), + TransactionHash.fromHexString(transactionHash), TX_TIMEOUT ); @@ -126,18 +150,18 @@ export default function SubmittedTransaction() { } }, undefined, - [txHash, grpc] + [transactionHash, grpc] ); - if (txHash === undefined) { + if (transactionHash === undefined) { return ; } // FIXME: // 1. translations... return ( - - + + {status === undefined && } {status?.type === 'success' && } {status?.type === 'failure' && ( @@ -149,6 +173,7 @@ export default function SubmittedTransaction() { } label="Transaction details" + className="submitted-tx__details-btn" leftLabel onClick={() => nav(transactionDetailsRoute(status.summary.sender, status.summary.hash))} /> diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/TokenAmount.tsx b/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/TokenAmount.tsx index 79ed84ea9..59bef513e 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/TokenAmount.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/TokenAmount.tsx @@ -15,20 +15,29 @@ const tokenAddressEq = (a: CIS2.TokenAddress | null, b: CIS2.TokenAddress | null return a === b; }; +type CcdBalanceType = 'total' | 'available'; + const balanceAtomFamily = atomFamily( - ([account, tokenAddress]: [AccountInfo, CIS2.TokenAddress | null]) => { + ([account, ccdBalance, tokenAddress]: [AccountInfo, CcdBalanceType, CIS2.TokenAddress | null]) => { if (tokenAddress === null) { - return atom(account.accountAvailableBalance.microCcdAmount); + return atom( + ccdBalance === 'available' + ? account.accountAvailableBalance.microCcdAmount + : account.accountAmount.microCcdAmount + ); } const tokens = contractBalancesFamily(account.accountAddress.address, tokenAddress.contract.index.toString()); return selectAtom(tokens, (ts) => ts[tokenAddress.id]); }, - ([aa, ta], [ab, tb]) => AccountAddress.equals(aa.accountAddress, ab.accountAddress) && tokenAddressEq(ta, tb) + ([aa, ba, ta], [ab, bb, tb]) => + AccountAddress.equals(aa.accountAddress, ab.accountAddress) && ba === bb && tokenAddressEq(ta, tb) ); type Props = Omit & { /** The account info of the account to take the amount from */ accountInfo: AccountInfo; + /** The ccd balance to use. Defaults to 'available' */ + ccdBalance?: CcdBalanceType; }; /** @@ -70,10 +79,10 @@ type Props = Omit * address={{ id: '', contract: ContractAddress.create(1) }} * /> */ -export default function TokenAmount({ accountInfo, ...props }: Props) { +export default function TokenAmount({ accountInfo, ccdBalance = 'available', ...props }: Props) { const tokenInfo = useTokenInfo(accountInfo.accountAddress); const [tokenAddress, setTokenAddress] = useState(null); - const tokenBalance = useAtomValue(balanceAtomFamily([accountInfo, tokenAddress])); + const tokenBalance = useAtomValue(balanceAtomFamily([accountInfo, ccdBalance, tokenAddress])); if (tokenInfo.loading) { return null; diff --git a/packages/browser-wallet/src/popup/popupX/styles/_elements.scss b/packages/browser-wallet/src/popup/popupX/styles/_elements.scss index 1da815976..8307c0cd5 100644 --- a/packages/browser-wallet/src/popup/popupX/styles/_elements.scss +++ b/packages/browser-wallet/src/popup/popupX/styles/_elements.scss @@ -26,6 +26,7 @@ @import '../pages/EarningRewards/Delegator/Stake/DelegatorStake'; @import '../pages/EarningRewards/Delegator/Result/DelegationResult'; @import '../pages/prompts/ConnectionRequest/ConnectionRequest'; +@import '../pages/SubmittedTransaction/SubmittedTransaction'; @import '../page-layouts/MainLayout'; @import '../page-layouts/ExternalRequestLayout/ExternalRequestLayout'; @import '../shared/Page/Page'; diff --git a/packages/browser-wallet/src/popup/shell/Root.tsx b/packages/browser-wallet/src/popup/shell/Root.tsx index b8977344b..676844ac3 100644 --- a/packages/browser-wallet/src/popup/shell/Root.tsx +++ b/packages/browser-wallet/src/popup/shell/Root.tsx @@ -136,7 +136,11 @@ export default function Root() { return ( - + From ffa505f41923be473d989a891fe03e39243b9e44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Thu, 24 Oct 2024 14:10:21 +0200 Subject: [PATCH 16/20] Handle pending/failed transactions on submission page --- .../SubmittedTransaction.scss | 12 +++++- .../SubmittedTransaction.tsx | 38 +++++++++---------- .../pages/SubmittedTransaction/i18n/en.ts | 22 +++++++++++ .../src/popup/popupX/shared/Loader/Loader.tsx | 8 +++- .../src/popup/popupX/shared/Loader/index.ts | 2 +- .../src/popup/shell/i18n/locales/en.ts | 2 + 6 files changed, 62 insertions(+), 22 deletions(-) create mode 100644 packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/i18n/en.ts diff --git a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.scss b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.scss index 7358d59ed..1406b5890 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.scss +++ b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.scss @@ -12,7 +12,7 @@ margin: rem(8px) 0; } - svg { + svg, .loader-x { margin-bottom: rem(30px); } } @@ -20,4 +20,14 @@ &__details-btn { margin-top: rem(24px); } + + &__failed-icon { + width: rem(48px); + height: rem(48px); + margin-bottom: rem(18px) !important; + + & path { + fill: $color-red-attention !important; + } + } } diff --git a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx index 866cf43de..7013a226d 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Navigate, useNavigate, useParams } from 'react-router-dom'; import CheckCircle from '@assets/svgX/check-circle.svg'; +import Cross from '@assets/svgX/close.svg'; import Arrow from '@assets/svgX/arrow-right.svg'; import Button from '@popup/popupX/shared/Button'; import Page from '@popup/popupX/shared/Page'; @@ -26,12 +27,15 @@ import { import { useAtomValue } from 'jotai'; import { grpcClientAtom } from '@popup/store/settings'; import { formatCcdAmount } from '@popup/popupX/shared/utils/helpers'; +import { LoaderInline } from '@popup/popupX/shared/Loader'; +import { useTranslation } from 'react-i18next'; const TX_TIMEOUT = 60 * 1000; // 1 minute type DelegationBodyProps = BaseAccountTransactionSummary & ConfigureDelegationSummary; function DelegationBody({ events }: DelegationBodyProps) { + const { t } = useTranslation('x', { keyPrefix: 'submittedTransaction.success.configureDelegation' }); const stakeChange = events.find((e) => [TransactionEventTag.DelegationStakeIncreased, TransactionEventTag.DelegationStakeDecreased].includes(e.tag) ) as DelegationStakeChangedEvent | undefined; @@ -39,7 +43,7 @@ function DelegationBody({ events }: DelegationBodyProps) { if (stakeChange !== undefined) { return ( <> - You’ve delegated + {t('changeStake')} {formatCcdAmount(stakeChange.newStake)} CCD @@ -51,10 +55,10 @@ function DelegationBody({ events }: DelegationBodyProps) { | undefined; if (removal !== undefined) { - return You’ve removed your delegated stake; + return {t('removed')}; } - return You’ve updated your delegation settings; + return {t('updated')}; } type SuccessSummary = Exclude; @@ -69,16 +73,15 @@ type SuccessProps = { tx: SuccessSummary; }; -// TODO: -// 1. Handle delegation transaction case function Success({ tx }: SuccessProps) { + const { t } = useTranslation('x', { keyPrefix: 'submittedTransaction' }); return ( <> {tx.transactionType === TransactionKindString.Transfer && ( <> - You’ve sent - 12,600.00 + {t('success.transfer.label')} + {formatCcdAmount(tx.transfer.amount)} CCD )} @@ -90,24 +93,21 @@ type FailureProps = { message: string; }; -// TODO: -// 1. Proper error icon function Failure({ message }: FailureProps) { return ( <> - + {message} ); } -// TODO: -// 1. Proper error icon function Finalizing() { + const { t } = useTranslation('x', { keyPrefix: 'submittedTransaction' }); return ( <> - - Finalizing on chain + + {t('pending.label')} ); } @@ -117,7 +117,9 @@ export type SubmittedTransactionParams = { transactionHash: HexString; }; +/** Component displaying the status of a submitted transaction. Must be given a transaction hash as a route parameter */ export default function SubmittedTransaction() { + const { t } = useTranslation('x', { keyPrefix: 'submittedTransaction' }); const { transactionHash } = useParams(); const nav = useNavigate(); const grpc = useAtomValue(grpcClientAtom); @@ -157,22 +159,20 @@ export default function SubmittedTransaction() { return ; } - // FIXME: - // 1. translations... return ( {status === undefined && } {status?.type === 'success' && } {status?.type === 'failure' && ( - + )} {status?.type === 'error' && } {status?.type !== undefined && status.type !== 'error' && ( } - label="Transaction details" + label={t('detailsButton')} className="submitted-tx__details-btn" leftLabel onClick={() => nav(transactionDetailsRoute(status.summary.sender, status.summary.hash))} @@ -182,7 +182,7 @@ export default function SubmittedTransaction() { nav(absoluteRoutes.home.path)} - label="Return to Account" + label={t('continue')} /> diff --git a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/i18n/en.ts b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/i18n/en.ts new file mode 100644 index 000000000..8d1b514a9 --- /dev/null +++ b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/i18n/en.ts @@ -0,0 +1,22 @@ +const t = { + success: { + configureDelegation: { + changeStake: "You've delegated", + removed: "You've removed your delegated stake", + updated: "You've updated your delegation settings", + }, + transfer: { + label: "You've sent", + }, + }, + pending: { + label: 'Transaction in progress', + }, + failure: { + label: 'The transaction failed: {{reason}}', + }, + detailsButton: 'Transaction details', + continue: 'Return to account', +}; + +export default t; diff --git a/packages/browser-wallet/src/popup/popupX/shared/Loader/Loader.tsx b/packages/browser-wallet/src/popup/popupX/shared/Loader/Loader.tsx index 35a4c7341..607cdb93c 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Loader/Loader.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/Loader/Loader.tsx @@ -1,9 +1,15 @@ +import clsx from 'clsx'; import React from 'react'; +import { ClassName } from 'wallet-common-helpers'; + +export function LoaderInline({ className }: ClassName) { + return ; +} export default function Loader() { return (
- +
); } diff --git a/packages/browser-wallet/src/popup/popupX/shared/Loader/index.ts b/packages/browser-wallet/src/popup/popupX/shared/Loader/index.ts index 348c02a98..70ad52a7b 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Loader/index.ts +++ b/packages/browser-wallet/src/popup/popupX/shared/Loader/index.ts @@ -1 +1 @@ -export { default } from './Loader'; +export { default, LoaderInline } from './Loader'; diff --git a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts index 8b6dd2cef..8f906902c 100644 --- a/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts +++ b/packages/browser-wallet/src/popup/shell/i18n/locales/en.ts @@ -48,6 +48,7 @@ import header from '@popup/popupX/page-layouts/MainLayout/Header/i18n/en'; import web3Id from '@popup/popupX/pages/Web3Id/i18n/en'; import earn from '@popup/popupX/pages/EarningRewards/i18n/en'; import connectionRequestX from '@popup/popupX/pages/prompts/ConnectionRequest/i18n/en'; +import submittedTransaction from '@popup/popupX/pages/SubmittedTransaction/i18n/en'; const t = { shared, @@ -99,6 +100,7 @@ const t = { web3Id, earn, prompts: { connectionRequestX }, + submittedTransaction, }, }; From 4459fb1fe4bf0854aeb39c5bdae207ea4c35bcd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Thu, 24 Oct 2024 14:23:32 +0200 Subject: [PATCH 17/20] Cleanup --- .../src/popup/popupX/constants/routes.ts | 9 +- .../TransactionFlow/TransactionFlow.tsx | 2 +- .../popupX/pages/SendFunds/SendConfirm.tsx | 8 +- .../popupX/pages/SendFunds/SendSuccess.tsx | 24 --- .../src/popup/popupX/pages/SendFunds/index.ts | 1 - .../src/popup/popupX/shared/MultiStepForm.tsx | 180 ------------------ .../src/popup/popupX/shell/Routes.tsx | 6 +- .../src/popup/shared/MultiStepForm.tsx | 13 ++ 8 files changed, 24 insertions(+), 219 deletions(-) delete mode 100644 packages/browser-wallet/src/popup/popupX/pages/SendFunds/SendSuccess.tsx delete mode 100644 packages/browser-wallet/src/popup/popupX/shared/MultiStepForm.tsx diff --git a/packages/browser-wallet/src/popup/popupX/constants/routes.ts b/packages/browser-wallet/src/popup/popupX/constants/routes.ts index 354ff2d06..e1f5e5cf7 100644 --- a/packages/browser-wallet/src/popup/popupX/constants/routes.ts +++ b/packages/browser-wallet/src/popup/popupX/constants/routes.ts @@ -71,12 +71,6 @@ export const relativeRoutes = { config: { backTitle: 'to Send Funds form', }, - confirmed: { - path: 'confirmed', - config: { - hideBackArrow: true, - }, - }, }, }, receive: { @@ -96,6 +90,9 @@ export const relativeRoutes = { }, submittedTransaction: { path: 'submitted/:transactionHash', + config: { + hideBackArrow: true, + }, }, }, settings: { diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx index fc9ad4870..e55899a1c 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx @@ -2,8 +2,8 @@ import React, { useCallback } from 'react'; import { Location, Navigate, useLocation, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import MultiStepForm from '@popup/popupX/shared/MultiStepForm'; import { absoluteRoutes } from '@popup/popupX/constants/routes'; +import MultiStepForm from '@popup/shared/MultiStepForm'; import DelegatorStake from '../Stake'; import DelegatorType from '../Type'; import { configureDelegatorPayloadFromForm, type DelegatorForm } from '../util'; diff --git a/packages/browser-wallet/src/popup/popupX/pages/SendFunds/SendConfirm.tsx b/packages/browser-wallet/src/popup/popupX/pages/SendFunds/SendConfirm.tsx index d4fd76fa3..a930e37dd 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/SendFunds/SendConfirm.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/SendFunds/SendConfirm.tsx @@ -2,11 +2,15 @@ import React from 'react'; import Arrow from '@assets/svgX/arrow-right.svg'; import Button from '@popup/popupX/shared/Button'; import { useNavigate } from 'react-router-dom'; -import { relativeRoutes } from '@popup/popupX/constants/routes'; +import { submittedTransactionRoute } from '@popup/popupX/constants/routes'; +import { TransactionHash } from '@concordium/web-sdk'; export default function SendConfirm() { const nav = useNavigate(); - const navToConfirmed = () => nav(relativeRoutes.home.send.confirmation.confirmed.path); + // TODO: + // 1. Submit transaction (see `Delegator/TransactionFlow`) + // 2. Pass the transaction hash to the route function below + const navToConfirmed = () => nav(submittedTransactionRoute(TransactionHash.fromHexString('..'))); return (
diff --git a/packages/browser-wallet/src/popup/popupX/pages/SendFunds/SendSuccess.tsx b/packages/browser-wallet/src/popup/popupX/pages/SendFunds/SendSuccess.tsx deleted file mode 100644 index 90ca7ca17..000000000 --- a/packages/browser-wallet/src/popup/popupX/pages/SendFunds/SendSuccess.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import CheckCircle from '@assets/svgX/check-circle.svg'; -import Arrow from '@assets/svgX/arrow-right.svg'; -import Button from '@popup/popupX/shared/Button'; -import { useNavigate } from 'react-router-dom'; - -export default function SendSuccess() { - const nav = useNavigate(); - return ( -
-
- - You’ve sent - 12,600.00 - CCD -
-
- Transaction details - -
- nav('../../../../home')} label="Return to Account" /> -
- ); -} diff --git a/packages/browser-wallet/src/popup/popupX/pages/SendFunds/index.ts b/packages/browser-wallet/src/popup/popupX/pages/SendFunds/index.ts index e45486115..eb40e6ac0 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/SendFunds/index.ts +++ b/packages/browser-wallet/src/popup/popupX/pages/SendFunds/index.ts @@ -1,3 +1,2 @@ export { default as SendFunds } from './SendFunds'; export { default as SendConfirm } from './SendConfirm'; -export { default as SendSuccess } from './SendSuccess'; diff --git a/packages/browser-wallet/src/popup/popupX/shared/MultiStepForm.tsx b/packages/browser-wallet/src/popup/popupX/shared/MultiStepForm.tsx deleted file mode 100644 index e73bc208b..000000000 --- a/packages/browser-wallet/src/popup/popupX/shared/MultiStepForm.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import React, { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react'; -import { Routes, useNavigate, Route, useLocation } from 'react-router-dom'; -import { isDefined, noOp, useUpdateEffect } from 'wallet-common-helpers'; - -const INDEX_ROUTE = '.'; - -export interface MultiStepFormPageProps { - /** - * Function to be triggered on page submission. Will take user to next page in the flow. - */ - onNext(values: V): void; - /** - * Initial values for substate. - */ - initial: V | undefined; - /** - * Accumulated values of entire flow (thus far) - */ - formValues: Partial; -} - -const makeFormPageObjects = >(children: FormChildren) => { - const keyPagePairs = Object.entries(children).filter(([, c]) => isDefined(c)); - - return keyPagePairs.map(([k, c]: [keyof F, FormChild], i) => ({ - substate: k, - render: c.render, - route: i === 0 ? INDEX_ROUTE : `${i}`, - })); -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface FormChild { - /** - * Function to render page component responsible for letting user fill out the respective substate. - * This is a function to avoid anonymous components messing up render tree updates. - */ - render(initial: F[K] | undefined, onNext: (values: F[K]) => void, formValues: Partial): JSX.Element; -} - -export type FormChildren> = { - [K in keyof F]?: FormChild; -}; - -/** - * Helper type to generate type for children expected by MultiStepForm - */ -export type OrRenderValues, C extends FormChildren> = - | C - | ((values: Partial) => C); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ValidateValues> = (values: F) => keyof F | undefined; - -interface Props> { - /** - * Function to validate the transaction flow values as a whole. - * Return key of the substate containing the invalid field, or undefined if valid - */ - validate?: ValidateValues; - onDone(values: F): void; - onPageActive?(step: keyof F, values: Partial): void; - /** - * Pages of the transaction flow declared as a mapping of components to corresponding substate. - * Declaration order defines the order the pages are shown. - */ - children: OrRenderValues>; -} - -interface InternalValueStoreProps> extends Props { - /** - * Initial values for the form. - */ - initialValues?: F; -} - -interface ExternalValueStoreProps> extends Props { - /** - * Matches the return type of "useState" hook - */ - valueStore: [Partial, Dispatch>>]; -} - -/** - * Props for multi step form component. Can either use an internal or external value store, which simply matches the tuple returned from the "useState" hook - * - * @template F Type of the form as a whole. Each step in the form flow should correspond to a member on the type. - */ -export type MultiStepFormProps> = - | InternalValueStoreProps - | ExternalValueStoreProps; - -/** - * A component for spanning forms over multiple pages. This component doesn't render any UI, but merely handles collecting data from the different steps and routing between the steps. - * - * @template F Type of the form as a whole. Each step in the form flow should correspond to a member on the type. - * @component - * @example - * type Values = { - * first: { a: string; b: number; }; - * second: { c: boolean; }; - * }; - * - * > - * {{ - * first: { render: (initialValues, onNext) => }, - * second: { render: (initialValues, onNext) => }, - * }} - * - */ -export default function MultiStepForm< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - F extends Record ->(props: MultiStepFormProps) { - const { children, validate = () => undefined, onDone, onPageActive = noOp } = props; - const initialValues = (props as InternalValueStoreProps).initialValues ?? ({} as F); - const { pathname } = useLocation(); - const internalValueStore = useState>(initialValues); - const externalValueStore = (props as ExternalValueStoreProps).valueStore; - const [values, setValues] = externalValueStore ?? internalValueStore; - const nav = useNavigate(); - - const getChildren = useCallback( - (v: Partial) => (typeof children === 'function' ? children(v) : children), - [children] - ); - - const pages = useMemo(() => makeFormPageObjects(getChildren(values)), [getChildren, values]); - const currentPage = pages.find((p) => pathname.endsWith(p.route)) ?? pages[0]; - - useEffect(() => { - if (currentPage?.substate) { - onPageActive(currentPage?.substate, values); - } - }, [currentPage?.substate]); - - useUpdateEffect(() => { - throw new Error('Changing value store during the lifetime of MultiStepForm will result in errors.'); - }, [externalValueStore === undefined]); - - const handleNext = (substate: keyof F) => (v: Partial) => { - const newValues = { ...values, [substate]: v }; - setValues(newValues); - - const newPages = makeFormPageObjects(getChildren(newValues)); - const currentIndex = newPages.findIndex((p) => p.substate === substate); - - if (currentIndex === -1) { - // Could not find current page. Should not happen. - // TODO: Log error. - nav(INDEX_ROUTE, { replace: true }); - } else if (currentIndex !== newPages.length - 1) { - // From any page that isn't the last, to the next in line. - const { route } = newPages[currentIndex + 1] ?? {}; - nav(route); - } else { - // On final page. Do validation -> trigger done. - const invalidPage = pages.find((p) => p.substate === validate(newValues as F)); - - if (invalidPage) { - nav(invalidPage.route); - return; - } - - onDone(newValues as F); - } - }; - - return ( - - {pages.map(({ render, route, substate }) => - route === INDEX_ROUTE ? ( - - ) : ( - - ) - )} - - ); -} diff --git a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx index f628826a9..7fc031ed3 100644 --- a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx @@ -3,7 +3,7 @@ import { Route, Routes as ReactRoutes } from 'react-router-dom'; import { relativeRoutes, routePrefix } from '@popup/popupX/constants/routes'; import MainLayout from '@popup/popupX/page-layouts/MainLayout'; import MainPage from '@popup/popupX/pages/MainPage'; -import { SendConfirm, SendFunds, SendSuccess } from '@popup/popupX/pages/SendFunds'; +import { SendConfirm, SendFunds } from '@popup/popupX/pages/SendFunds'; import ReceiveFunds from '@popup/popupX/pages/ReceiveFunds'; import TransactionLog from '@popup/popupX/pages/TransactionLog'; import TransactionDetails from '@popup/popupX/pages/TransactionDetails'; @@ -50,10 +50,6 @@ export default function Routes({ messagePromptHandlers }: { messagePromptHandler } /> } /> - } - path={relativeRoutes.home.send.confirmation.confirmed.path} - /> } path={relativeRoutes.home.receive.path} /> diff --git a/packages/browser-wallet/src/popup/shared/MultiStepForm.tsx b/packages/browser-wallet/src/popup/shared/MultiStepForm.tsx index 42873c28b..e73bc208b 100644 --- a/packages/browser-wallet/src/popup/shared/MultiStepForm.tsx +++ b/packages/browser-wallet/src/popup/shared/MultiStepForm.tsx @@ -94,6 +94,19 @@ export type MultiStepFormProps> = * A component for spanning forms over multiple pages. This component doesn't render any UI, but merely handles collecting data from the different steps and routing between the steps. * * @template F Type of the form as a whole. Each step in the form flow should correspond to a member on the type. + * @component + * @example + * type Values = { + * first: { a: string; b: number; }; + * second: { c: boolean; }; + * }; + * + * > + * {{ + * first: { render: (initialValues, onNext) => }, + * second: { render: (initialValues, onNext) => }, + * }} + * */ export default function MultiStepForm< // eslint-disable-next-line @typescript-eslint/no-explicit-any From 2144deb98ce02f1b86e0effacbd752dd35c13854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Fri, 25 Oct 2024 09:21:59 +0200 Subject: [PATCH 18/20] Use text components instead of css classes --- .../EarningRewards/Baker/Intro/BakerIntro.tsx | 13 +- .../Delegator/Result/DelegationResult.tsx | 6 +- .../Delegator/Stake/DelegatorStake.tsx | 17 ++- .../pages/EarningRewards/EarningRewards.tsx | 13 +- .../SubmittedTransaction.tsx | 21 +-- .../popupX/shared/Form/TokenAmount/View.tsx | 11 +- .../shared/utils/transaction-helpers.ts | 141 ------------------ 7 files changed, 42 insertions(+), 180 deletions(-) delete mode 100644 packages/browser-wallet/src/popup/popupX/shared/utils/transaction-helpers.ts diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Baker/Intro/BakerIntro.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Baker/Intro/BakerIntro.tsx index ffc59a52a..edd653ed9 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Baker/Intro/BakerIntro.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Baker/Intro/BakerIntro.tsx @@ -3,6 +3,7 @@ import Carousel from '@popup/popupX/shared/Carousel'; import { absoluteRoutes } from '@popup/popupX/constants/routes'; import { useNavigate } from 'react-router-dom'; import Page from '@popup/popupX/shared/Page'; +import Text from '@popup/popupX/shared/Text'; import { Trans, useTranslation } from 'react-i18next'; export default function BakerIntro() { @@ -11,18 +12,18 @@ export default function BakerIntro() { return ( nav(absoluteRoutes.settings.earn.validator.register.path)}> - + - - + + - - + + - + ); diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx index 13940a4d4..12b6703f9 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx @@ -11,6 +11,7 @@ import { import { Navigate, useLocation, Location, useNavigate } from 'react-router-dom'; import { useAtomValue } from 'jotai'; import { useTranslation } from 'react-i18next'; +import { useUpdateAtom } from 'jotai/utils'; import { selectedAccountAtom } from '@popup/store/account'; import Button from '@popup/popupX/shared/Button'; @@ -20,7 +21,6 @@ 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 { grpcClientAtom } from '@popup/store/settings'; import { usePrivateKey } from '@popup/shared/utils/account-helpers'; import { @@ -29,11 +29,11 @@ import { getTransactionAmount, sendTransaction, } from '@popup/shared/utils/transaction-helpers'; -import { useUpdateAtom } from 'jotai/utils'; import { addPendingTransactionAtom } from '@popup/store/transactions'; import { cpStakingCooldown } from '@shared/utils/chain-parameters-helpers'; import { useGetTransactionFee } from '@popup/popupX/shared/utils/transaction-helpers'; import { submittedTransactionRoute } from '@popup/popupX/constants/routes'; +import Text from '@popup/popupX/shared/Text'; enum TransactionSubmitErrorType { InsufficientFunds = 'InsufficientFunds', @@ -142,7 +142,7 @@ export default function DelegationResult() { return ( - {notice} + {notice} diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx index da5a798b9..7a9a3fa6f 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx @@ -16,6 +16,7 @@ import { formatTokenAmount } from '@popup/popupX/shared/utils/helpers'; import { CCD_METADATA } from '@shared/constants/token-metadata'; import { grpcClientAtom } from '@popup/store/settings'; import { useGetTransactionFee } from '@popup/popupX/shared/utils/transaction-helpers'; +import Text from '@popup/popupX/shared/Text'; import { DelegationTypeForm, DelegatorStakeForm, configureDelegatorPayloadFromForm } from '../util'; @@ -38,14 +39,14 @@ function PoolInfo({ validatorId }: PoolInfoProps) { return (
- {t('poolStake.label')} - + {t('poolStake.label')} + {t('poolStake.value', { amount: formatTokenAmount(poolStake, CCD_METADATA.decimals, 2) })} - +
- {t('poolCap.label')} - + {t('poolCap.label')} + {t('poolCap.value', { amount: formatTokenAmount( poolStatus.delegatedCapitalCap!.microCcdAmount, @@ -53,7 +54,7 @@ function PoolInfo({ validatorId }: PoolInfoProps) { 2 ), })} - +
); @@ -110,10 +111,10 @@ export default function DelegatorStake({ title, target, initialValues, onSubmit )}
- {t('redelegate.label')} + {t('redelegate.label')}
- {t('redelegate.description')} + {t('redelegate.description')}
)} diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/EarningRewards.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/EarningRewards.tsx index 24fc6c79e..c6cf741bb 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/EarningRewards.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/EarningRewards.tsx @@ -8,6 +8,7 @@ import { displayAsCcd } from 'wallet-common-helpers'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { absoluteRoutes } from '@popup/popupX/constants/routes'; +import Text from '@popup/popupX/shared/Text'; export default function EarningRewards() { const { t } = useTranslation('x', { keyPrefix: 'earn.root' }); @@ -24,10 +25,10 @@ export default function EarningRewards() {
- {t('validatorTitle')} - + {t('validatorTitle')} + {t('validatorDescription', { amount: displayAsCcd(bakingThreshold, false) })} - +
{t('validatorAction')} @@ -36,8 +37,8 @@ export default function EarningRewards() {
- {t('delegationTitle')} - {t('delegationDescription')} + {t('delegationTitle')} + {t('delegationDescription')}
{t('delegationAction')} @@ -49,7 +50,7 @@ export default function EarningRewards() {
- {t('note')} + {t('note')}
diff --git a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx index 7013a226d..1e125d87d 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/SubmittedTransaction/SubmittedTransaction.tsx @@ -29,6 +29,7 @@ import { grpcClientAtom } from '@popup/store/settings'; import { formatCcdAmount } from '@popup/popupX/shared/utils/helpers'; import { LoaderInline } from '@popup/popupX/shared/Loader'; import { useTranslation } from 'react-i18next'; +import Text from '@popup/popupX/shared/Text'; const TX_TIMEOUT = 60 * 1000; // 1 minute @@ -43,9 +44,9 @@ function DelegationBody({ events }: DelegationBodyProps) { if (stakeChange !== undefined) { return ( <> - {t('changeStake')} - {formatCcdAmount(stakeChange.newStake)} - CCD + {t('changeStake')} + {formatCcdAmount(stakeChange.newStake)} + CCD ); } @@ -55,10 +56,10 @@ function DelegationBody({ events }: DelegationBodyProps) { | undefined; if (removal !== undefined) { - return {t('removed')}; + return {t('removed')}; } - return {t('updated')}; + return {t('updated')}; } type SuccessSummary = Exclude; @@ -80,9 +81,9 @@ function Success({ tx }: SuccessProps) { {tx.transactionType === TransactionKindString.Transfer && ( <> - {t('success.transfer.label')} - {formatCcdAmount(tx.transfer.amount)} - CCD + {t('success.transfer.label')} + {formatCcdAmount(tx.transfer.amount)} + CCD )} {tx.transactionType === TransactionKindString.ConfigureDelegation && } @@ -97,7 +98,7 @@ function Failure({ message }: FailureProps) { return ( <> - {message} + {message} ); } @@ -107,7 +108,7 @@ function Finalizing() { return ( <> - {t('pending.label')} + {t('pending.label')} ); } diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx b/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx index 20618d4fe..27aee63f2 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx @@ -18,6 +18,7 @@ import Button from '../../Button'; import { formatTokenAmount, parseTokenAmount, removeNumberGrouping } from '../../utils/helpers'; import ErrorMessage from '../ErrorMessage'; import { TokenInfo } from './util'; +import Text from '@popup/popupX/shared/Text'; type AmountInputProps = Pick< InputHTMLAttributes, @@ -160,15 +161,15 @@ function TokenPicker({ )}
{token.icon}
- {token.name} + {token.name} {canSelect && } {selectedTokenBalance !== undefined && ( - + {t('form.tokenAmount.token.available', { balance: formatAmount(selectedTokenBalance), })} - + )}
); @@ -394,9 +395,7 @@ export default function TokenAmountView(props: TokenAmountViewProps) { {props.form.formState.errors.amount?.message} - - {t('form.tokenAmount.amount.fee', { fee: displayAsCcd(fee, false, true) })} - + {t('form.tokenAmount.amount.fee', { fee: displayAsCcd(fee, false, true) })}
{props.receiver === true && (
diff --git a/packages/browser-wallet/src/popup/popupX/shared/utils/transaction-helpers.ts b/packages/browser-wallet/src/popup/popupX/shared/utils/transaction-helpers.ts deleted file mode 100644 index af6eeb720..000000000 --- a/packages/browser-wallet/src/popup/popupX/shared/utils/transaction-helpers.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { - AccountAddress, - AccountInfo, - AccountInfoType, - AccountTransactionType, - BakerPoolStatusDetails, - ChainParameters, - ChainParametersV0, - ConfigureDelegationPayload, - convertEnergyToMicroCcd, - getEnergyCost, -} from '@concordium/web-sdk'; -import { useBlockChainParameters } from '@popup/shared/BlockChainParametersProvider'; -import i18n from '@popup/shell/i18n'; -import { useCallback } from 'react'; -import { - ccdToMicroCcd, - displayAsCcd, - fractionalToInteger, - getPublicAccountAmounts, - isValidCcdString, - isValidResolutionString, -} from 'wallet-common-helpers'; - -/** - * Validates if the chosen transfer amount can be sent with the current balance at disposal. - * @param decimals how many decimals can the transfer amount. This is used to convert it from a fractional string to an integer. - * @param estimatedFee additional costs for the transfer. - */ -export function validateTransferAmount( - transferAmount: string, - atDisposal: bigint | undefined, - decimals = 0, - estimatedFee = 0n -): string | undefined { - if (!isValidResolutionString(10n ** BigInt(decimals), false, false, false)(transferAmount)) { - return i18n.t('x:sharedX.utils.amount.invalid'); - } - const amountToValidateInteger = fractionalToInteger(transferAmount, decimals); - if (atDisposal !== undefined && atDisposal < amountToValidateInteger + estimatedFee) { - return i18n.t('x:sharedX.utils.amount.insufficient'); - } - if (amountToValidateInteger === 0n) { - return i18n.t('x:sharedX.utils.amount.zero'); - } - return undefined; -} - -export function validateBakerStake( - amountToValidate: string, - chainParameters?: Exclude, - accountInfo?: AccountInfo, - estimatedFee = 0n -): string | undefined { - if (!isValidCcdString(amountToValidate)) { - return i18n.t('x:sharedX.utils.amount.invalid'); - } - const bakerStakeThreshold = chainParameters?.minimumEquityCapital.microCcdAmount || 0n; - const amount = ccdToMicroCcd(amountToValidate); - - const amountChanged = - accountInfo?.type !== AccountInfoType.Baker || amount !== accountInfo.accountBaker.stakedAmount.microCcdAmount; - - if (amountChanged && bakerStakeThreshold > amount) { - return i18n.t('x:sharedX.utils.amount.belowBakerThreshold', { - threshold: displayAsCcd(bakerStakeThreshold, false), - }); - } - - if ( - accountInfo && - (BigInt(accountInfo.accountAmount.microCcdAmount) < amount + estimatedFee || - // the fee must be paid with the current funds at disposal, because a reduction in delegation amount is not immediate. - getPublicAccountAmounts(accountInfo).atDisposal < estimatedFee) - ) { - return i18n.t('x:sharedX.utils.amount.insufficient'); - } - - return undefined; -} - -export function validateAccountAddress(cand: string): string | undefined { - try { - // eslint-disable-next-line no-new - AccountAddress.fromBase58(cand); - return undefined; - } catch { - return i18n.t('x:sharedX.utils.address.invalid'); - } -} - -export function validateDelegationAmount( - delegatedAmount: string, - accountInfo: AccountInfo, - estimatedFee: bigint, - targetStatus?: BakerPoolStatusDetails -): string | undefined { - if (!isValidCcdString(delegatedAmount)) { - return i18n.t('x:sharedX.utils.amount.invalid'); - } - - const amount = ccdToMicroCcd(delegatedAmount); - - if (amount === 0n) { - return i18n.t('x:sharedX.utils.amount.zero'); - } - - const max = - targetStatus && targetStatus.delegatedCapitalCap && targetStatus.delegatedCapital - ? targetStatus.delegatedCapitalCap.microCcdAmount - targetStatus.delegatedCapital.microCcdAmount - : undefined; - if (max !== undefined && amount > max) { - return i18n.t('x:sharedX.utils.amount.exceedingDelegationCap', { max: displayAsCcd(max) }); - } - - if ( - BigInt(accountInfo.accountAmount.microCcdAmount) < amount + estimatedFee || - // the fee must be paid with the current funds at disposal, because a reduction in delegation amount is not immediate. - getPublicAccountAmounts(accountInfo).atDisposal < estimatedFee - ) { - return i18n.t('x:sharedX.utils.amount.insufficient'); - } - - return undefined; -} - -/** Hook which exposes a function for getting the transaction fee for a given transaction type */ -export function useGetTransactionFee(type: AccountTransactionType) { - const cp = useBlockChainParameters(); - - return useCallback( - (payload: ConfigureDelegationPayload) => { - if (cp === undefined) { - return undefined; - } - const energy = getEnergyCost(type, payload); - return convertEnergyToMicroCcd(energy, cp); - }, - [cp, type] - ); -} From 4ec0eec2c653b8bc87da4739f31947001b0212f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Fri, 25 Oct 2024 10:00:19 +0200 Subject: [PATCH 19/20] Move routes to top level router spec --- .../src/popup/popupX/constants/routes.ts | 3 +++ .../Delegator/RegisterDelegator.tsx | 17 -------------- .../Delegator/Result/DelegationResult.tsx | 2 +- .../Delegator/Stake/DelegatorStake.tsx | 15 ++++++------ .../TransactionFlow/TransactionFlow.tsx | 2 +- .../popupX/shared/Form/TokenAmount/View.tsx | 4 ++-- .../src/popup/popupX/shell/Routes.tsx | 23 ++++++++++++++----- .../src/popup/shared/MultiStepForm.tsx | 7 +++++- .../popup/shared/utils/transaction-helpers.ts | 20 ++++++++++++++++ 9 files changed, 57 insertions(+), 36 deletions(-) delete mode 100644 packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/RegisterDelegator.tsx diff --git a/packages/browser-wallet/src/popup/popupX/constants/routes.ts b/packages/browser-wallet/src/popup/popupX/constants/routes.ts index e1f5e5cf7..3ae6e29b8 100644 --- a/packages/browser-wallet/src/popup/popupX/constants/routes.ts +++ b/packages/browser-wallet/src/popup/popupX/constants/routes.ts @@ -173,6 +173,9 @@ export const relativeRoutes = { /** Configure new delegator */ register: { path: 'register', + configure: { + path: 'configure', + }, }, /** Configure existing delegator */ update: { diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/RegisterDelegator.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/RegisterDelegator.tsx deleted file mode 100644 index 4c25e7235..000000000 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/RegisterDelegator.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { Route, Routes } from 'react-router-dom'; -import { DelegatorIntro } from './Intro'; -import TransactionFlow from './TransactionFlow'; - -const routes = { - configure: 'configure', -}; - -export default function RegisterDelegator() { - return ( - - } /> - } /> - - ); -} diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx index 12b6703f9..05402fee0 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx @@ -28,10 +28,10 @@ import { getDefaultExpiry, getTransactionAmount, sendTransaction, + useGetTransactionFee, } from '@popup/shared/utils/transaction-helpers'; import { addPendingTransactionAtom } from '@popup/store/transactions'; import { cpStakingCooldown } from '@shared/utils/chain-parameters-helpers'; -import { useGetTransactionFee } from '@popup/popupX/shared/utils/transaction-helpers'; import { submittedTransactionRoute } from '@popup/popupX/constants/routes'; import Text from '@popup/popupX/shared/Text'; diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx index 7a9a3fa6f..ebbd4b8ab 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Stake/DelegatorStake.tsx @@ -15,8 +15,8 @@ import { displayNameAndSplitAddress, useSelectedCredential } from '@popup/shared import { formatTokenAmount } from '@popup/popupX/shared/utils/helpers'; import { CCD_METADATA } from '@shared/constants/token-metadata'; import { grpcClientAtom } from '@popup/store/settings'; -import { useGetTransactionFee } from '@popup/popupX/shared/utils/transaction-helpers'; import Text from '@popup/popupX/shared/Text'; +import { useGetTransactionFee } from '@popup/shared/utils/transaction-helpers'; import { DelegationTypeForm, DelegatorStakeForm, configureDelegatorPayloadFromForm } from '../util'; @@ -78,12 +78,11 @@ export default function DelegatorStake({ title, target, initialValues, onSubmit const submit = form.handleSubmit(onSubmit); const selectedCred = useSelectedCredential(); const selectedAccountInfo = useAccountInfo(selectedCred); - const { amount, redelegate } = form.watch(); const getCost = useGetTransactionFee(AccountTransactionType.ConfigureDelegation); - const fee = useMemo(() => { - const payload = configureDelegatorPayloadFromForm({ target, stake: { amount, redelegate } }); - return getCost(payload); - }, [target, amount, redelegate, getCost]); + const fee = useMemo( + () => getCost(configureDelegatorPayloadFromForm({ target, stake: { amount: '0', redelegate: true } })), // Use dummy values, as it does not matter when calculating transaction cost + [target, getCost] + ); if (selectedAccountInfo === undefined || selectedCred === undefined || fee === undefined) { return null; @@ -92,9 +91,9 @@ export default function DelegatorStake({ title, target, initialValues, onSubmit return ( - + {t('selectedAccount', { account: displayNameAndSplitAddress(selectedCred) })} - + {(f) => ( <> diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx index e55899a1c..a055d8d9c 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/TransactionFlow/TransactionFlow.tsx @@ -9,7 +9,7 @@ import DelegatorType from '../Type'; import { configureDelegatorPayloadFromForm, type DelegatorForm } from '../util'; import { DelegationResultLocationState } from '../Result/DelegationResult'; -export default function TransactionFlow() { +export default function DelegatorTransactionFlow() { const { state: initialValues, pathname } = useLocation() as Location & { state: DelegatorForm | undefined }; const nav = useNavigate(); const { t } = useTranslation('x', { keyPrefix: 'earn.delegator.register' }); diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx b/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx index 27aee63f2..161382907 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx @@ -165,11 +165,11 @@ function TokenPicker({ {canSelect && } {selectedTokenBalance !== undefined && ( - + {t('form.tokenAmount.token.available', { balance: formatAmount(selectedTokenBalance), })} - + )}
); diff --git a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx index 7fc031ed3..406cbff90 100644 --- a/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx +++ b/packages/browser-wallet/src/popup/popupX/shell/Routes.tsx @@ -28,9 +28,10 @@ import { RestoreIntro, RestoreResult } from '@popup/popupX/pages/Restore'; import { MessagePromptHandlersType } from '@popup/shared/utils/message-prompt-handlers'; import ConnectionRequest from '@popup/popupX/pages/prompts/ConnectionRequest'; import ExternalRequestLayout from '@popup/popupX/page-layouts/ExternalRequestLayout'; -import RegisterDelegator from '../pages/EarningRewards/Delegator/RegisterDelegator'; import { DelegationResult } from '../pages/EarningRewards/Delegator/Result'; import SubmittedTransaction from '../pages/SubmittedTransaction'; +import { DelegatorIntro } from '../pages/EarningRewards/Delegator/Intro'; +import DelegatorTransactionFlow from '../pages/EarningRewards/Delegator/TransactionFlow'; export default function Routes({ messagePromptHandlers }: { messagePromptHandlers: MessagePromptHandlersType }) { const { handleConnectionResponse } = messagePromptHandlers; @@ -104,12 +105,22 @@ export default function Routes({ messagePromptHandlers }: { messagePromptHandler } path={relativeRoutes.settings.earn.validator.keys.path} /> + + + } + /> + } + /> + } - path={`${relativeRoutes.settings.earn.delegator.register.path}/*`} - /> - } // FIXME: change to update flow + element={} path={`${relativeRoutes.settings.earn.delegator.update.path}/*`} /> > = /** * A component for spanning forms over multiple pages. This component doesn't render any UI, but merely handles collecting data from the different steps and routing between the steps. + * The component uses the application router to go through a number of pages. As such it needs to be used in combination with a catch-all route, as seen in the example. * * @template F Type of the form as a whole. Each step in the form flow should correspond to a member on the type. * @component @@ -101,12 +102,16 @@ export type MultiStepFormProps> = * second: { c: boolean; }; * }; * - * > + * const Flow = () => > * {{ * first: { render: (initialValues, onNext) => }, * second: { render: (initialValues, onNext) => }, * }} * + * + * + * } /> + * */ export default function MultiStepForm< // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/browser-wallet/src/popup/shared/utils/transaction-helpers.ts b/packages/browser-wallet/src/popup/shared/utils/transaction-helpers.ts index 352379c0d..635d7e81d 100644 --- a/packages/browser-wallet/src/popup/shared/utils/transaction-helpers.ts +++ b/packages/browser-wallet/src/popup/shared/utils/transaction-helpers.ts @@ -18,6 +18,9 @@ import { UpdateContractPayload, SimpleTransferWithMemoPayload, AccountInfoType, + ConfigureDelegationPayload, + getEnergyCost, + convertEnergyToMicroCcd, } from '@concordium/web-sdk'; import { isValidResolutionString, @@ -33,6 +36,8 @@ import { useAtomValue } from 'jotai'; import { selectedPendingTransactionsAtom } from '@popup/store/transactions'; import { DEFAULT_TRANSACTION_EXPIRY } from '@shared/constants/time'; import { BrowserWalletAccountTransaction, TransactionStatus } from './transaction-history-types'; +import { useBlockChainParameters } from '../BlockChainParametersProvider'; +import { useCallback } from 'react'; export function buildSimpleTransferPayload(recipient: string, amount: bigint): SimpleTransferPayload { return { @@ -247,3 +252,18 @@ export function getTransactionAmount(type: AccountTransactionType, payload: Acco return 0n; } } +/** Hook which exposes a function for getting the transaction fee for a given transaction type */ +export function useGetTransactionFee(type: AccountTransactionType) { + const cp = useBlockChainParameters(); + + return useCallback( + (payload: ConfigureDelegationPayload) => { + if (cp === undefined) { + return undefined; + } + const energy = getEnergyCost(type, payload); + return convertEnergyToMicroCcd(energy, cp); + }, + [cp, type] + ); +} From 279bbe6efc5823b19a4d009208a87d2f39a0dabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Fri, 25 Oct 2024 10:09:58 +0200 Subject: [PATCH 20/20] Small fixes from PR suggestions --- .../src/popup/popupX/constants/routes.ts | 10 ++++++---- .../Delegator/Result/DelegationResult.tsx | 2 +- .../src/popup/popupX/shared/Form/TokenAmount/View.tsx | 2 +- .../src/popup/shared/utils/transaction-helpers.ts | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/browser-wallet/src/popup/popupX/constants/routes.ts b/packages/browser-wallet/src/popup/popupX/constants/routes.ts index 3ae6e29b8..7e7777e56 100644 --- a/packages/browser-wallet/src/popup/popupX/constants/routes.ts +++ b/packages/browser-wallet/src/popup/popupX/constants/routes.ts @@ -1,4 +1,5 @@ import { AccountAddress, TransactionHash } from '@concordium/web-sdk'; +import { generatePath } from 'react-router-dom'; export type RouteConfig = { hideBackArrow?: boolean; @@ -228,12 +229,13 @@ const buildAbsoluteRoutes = ( export const absoluteRoutes = buildAbsoluteRoutes(relativeRoutes); export const transactionDetailsRoute = (account: AccountAddress.Type, tx: TransactionHash.Type) => - absoluteRoutes.home.transactionLog.details.path - .replace(':account', account.address) - .replace(':transactionHash', TransactionHash.toHexString(tx)); + generatePath(absoluteRoutes.home.transactionLog.details.path, { + account: account.address, + transactionHash: TransactionHash.toHexString(tx), + }); export const submittedTransactionRoute = (tx: TransactionHash.Type) => - absoluteRoutes.home.submittedTransaction.path.replace(':transactionHash', TransactionHash.toHexString(tx)); + generatePath(absoluteRoutes.home.submittedTransaction.path, { transactionHash: TransactionHash.toHexString(tx) }); /** * Given two absolute routes, returns the relative route between them. diff --git a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx index 05402fee0..da55ab797 100644 --- a/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx +++ b/packages/browser-wallet/src/popup/popupX/pages/EarningRewards/Delegator/Result/DelegationResult.tsx @@ -42,7 +42,7 @@ enum TransactionSubmitErrorType { class TransactionSubmitError extends Error { private constructor(public type: TransactionSubmitErrorType) { super(); - super.name = `${'TransactionSubmitError'}.type`; + super.name = `TransactionSubmitError.${type}`; } public static insufficientFunds(): TransactionSubmitError { diff --git a/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx b/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx index 161382907..16a06991f 100644 --- a/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx +++ b/packages/browser-wallet/src/popup/popupX/shared/Form/TokenAmount/View.tsx @@ -12,13 +12,13 @@ import ConcordiumLogo from '@assets/svgX/concordium-logo.svg'; import { validateAccountAddress, validateTransferAmount } from '@popup/shared/utils/transaction-helpers'; import Img, { DEFAULT_FAILED } from '@popup/shared/Img'; import { displayAsCcd } from 'wallet-common-helpers'; +import Text from '@popup/popupX/shared/Text'; import { RequiredUncontrolledFieldProps } from '../common/types'; import { makeUncontrolled } from '../common/utils'; import Button from '../../Button'; import { formatTokenAmount, parseTokenAmount, removeNumberGrouping } from '../../utils/helpers'; import ErrorMessage from '../ErrorMessage'; import { TokenInfo } from './util'; -import Text from '@popup/popupX/shared/Text'; type AmountInputProps = Pick< InputHTMLAttributes, diff --git a/packages/browser-wallet/src/popup/shared/utils/transaction-helpers.ts b/packages/browser-wallet/src/popup/shared/utils/transaction-helpers.ts index 635d7e81d..ea8258e92 100644 --- a/packages/browser-wallet/src/popup/shared/utils/transaction-helpers.ts +++ b/packages/browser-wallet/src/popup/shared/utils/transaction-helpers.ts @@ -35,9 +35,9 @@ import i18n from '@popup/shell/i18n'; import { useAtomValue } from 'jotai'; import { selectedPendingTransactionsAtom } from '@popup/store/transactions'; import { DEFAULT_TRANSACTION_EXPIRY } from '@shared/constants/time'; +import { useCallback } from 'react'; import { BrowserWalletAccountTransaction, TransactionStatus } from './transaction-history-types'; import { useBlockChainParameters } from '../BlockChainParametersProvider'; -import { useCallback } from 'react'; export function buildSimpleTransferPayload(recipient: string, amount: bigint): SimpleTransferPayload { return {