From fb35cbf3753f246046d68b0eeaf63f909fec522b Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Mon, 21 Oct 2019 11:40:51 +0200 Subject: [PATCH] Staking Address func. (incl. performance improvements) (#1744) * Staking Address func. (incl. performance improvements) * Skip unneeded re-renders * Use reducer * Get rid on transform-only state * Don't re-create empty objects --- packages/app-staking/src/Overview/Address.tsx | 291 +++++++++--------- packages/app-staking/src/index.tsx | 97 +++--- 2 files changed, 184 insertions(+), 204 deletions(-) diff --git a/packages/app-staking/src/Overview/Address.tsx b/packages/app-staking/src/Overview/Address.tsx index e9e7089ff4..9838ecb754 100644 --- a/packages/app-staking/src/Overview/Address.tsx +++ b/packages/app-staking/src/Overview/Address.tsx @@ -1,17 +1,16 @@ -/* eslint-disable @typescript-eslint/camelcase */ // Copyright 2017-2019 @polkadot/app-staking authors & contributors // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { AccountId, Balance, BlockNumber, Exposure } from '@polkadot/types/interfaces'; +import { AccountId, Balance, BlockNumber } from '@polkadot/types/interfaces'; import { DerivedStaking, DerivedStakingOnlineStatus } from '@polkadot/api-derive/types'; -import { ApiProps } from '@polkadot/react-api/types'; import { I18nProps } from '@polkadot/react-components/types'; import { ValidatorFilter } from '../types'; -import React from 'react'; +import BN from 'bn.js'; +import React, { useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { withCalls, withMulti } from '@polkadot/react-api/with'; +import { ApiContext, withCalls, withMulti } from '@polkadot/react-api'; import { AddressCard, AddressMini, OnlineStatus } from '@polkadot/react-components'; import { classes } from '@polkadot/react-components/util'; import keyring from '@polkadot/ui-keyring'; @@ -20,7 +19,7 @@ import { updateOnlineStatus } from '../util'; import translate from '../translate'; -interface Props extends ApiProps, I18nProps { +interface Props extends I18nProps { address: string; className?: string; defaultName: string; @@ -28,171 +27,156 @@ interface Props extends ApiProps, I18nProps { lastAuthor: string; lastBlock: string; recentlyOnline?: Record; - staking_info?: DerivedStaking; + stakingInfo?: DerivedStaking; } -interface State { - controllerId: string | null; - onlineStatus: DerivedStakingOnlineStatus; +interface StakingState { + balanceOpts: { bonded: boolean | BN[] }; + controllerId?: string; + hasNominators: boolean; + nominators: [AccountId, Balance][]; stashActive: string | null; stashTotal: string | null; - sessionId: string | null; - stakers?: Exposure; - stashId: string | null; - badgeExpanded: boolean; + sessionId?: string; + stashId?: string; } -class Address extends React.PureComponent { - public state: State; +interface OnlineState { + hasOfflineWarnings: boolean; + onlineStatus: DerivedStakingOnlineStatus; +} - public constructor (props: Props) { - super(props); +const WITH_VALIDATOR_PREFS = { validatorPayment: true }; - this.state = { - controllerId: null, - onlineStatus: {}, - sessionId: null, - stashActive: null, - stashId: null, - stashTotal: null, - badgeExpanded: false - }; - } +function Address ({ address, className, defaultName, filter, lastAuthor, lastBlock, recentlyOnline, stakingInfo, t }: Props): React.ReactElement | null { + const { isSubstrateV2 } = useContext(ApiContext); + const [isNominatorMe, seIsNominatorMe] = useState(false); + const [{ hasOfflineWarnings, onlineStatus }, setOnlineStatus] = useState({ + hasOfflineWarnings: false, + onlineStatus: {} + }); + const [{ balanceOpts, controllerId, hasNominators, nominators, sessionId, stashId }, setStakingState] = useState({ + balanceOpts: { bonded: true }, + hasNominators: false, + nominators: [], + stashActive: null, + stashTotal: null + }); - public static getDerivedStateFromProps ({ recentlyOnline = {}, staking_info }: Props, prevState: State): Pick | null { - if (!staking_info) { - return null; + useEffect((): void => { + if (stakingInfo) { + const { controllerId, nextSessionId, stakers, stakingLedger, stashId } = stakingInfo; + const nominators = stakers + ? stakers.others.map(({ who, value }): [AccountId, Balance] => [who, value.unwrap()]) + : []; + const myAccounts = keyring.getAccounts().map(({ address }): string => address); + + seIsNominatorMe(nominators.some(([who]): boolean => + myAccounts.includes(who.toString()) + )); + setStakingState({ + balanceOpts: { + bonded: stakers && !stakers.own.isEmpty + ? [stakers.own.unwrap(), stakers.total.unwrap().sub(stakers.own.unwrap())] + : true + }, + controllerId: controllerId && controllerId.toString(), + hasNominators: nominators.length !== 0, + nominators, + sessionId: nextSessionId && nextSessionId.toString(), + stashActive: stakingLedger + ? formatBalance(stakingLedger.active) + : null, + stashId: stashId && stashId.toString(), + stashTotal: stakingLedger + ? formatBalance(stakingLedger.total) + : null + }); } + }, [stakingInfo]); - const { controllerId, nextSessionId, online, offline, stakers, stakingLedger, stashId } = staking_info; + useEffect((): void => { + if (stakingInfo) { + const { online, offline, sessionIds, stashId } = stakingInfo; + const onlineStatus = updateOnlineStatus(recentlyOnline || {})(sessionIds, { offline, online }); - return { - controllerId: controllerId && controllerId.toString(), - onlineStatus: updateOnlineStatus(recentlyOnline)(staking_info.sessionIds || null, { offline, online }), - sessionId: nextSessionId && nextSessionId.toString(), - stashActive: stakingLedger - ? formatBalance(stakingLedger.active) - : prevState.stashActive, - stakers, - stashId: stashId && stashId.toString(), - stashTotal: stakingLedger - ? formatBalance(stakingLedger.total) - : prevState.stashTotal - }; - } - - public render (): React.ReactNode { - const { address, className, defaultName, filter, isSubstrateV2, lastAuthor, lastBlock, t } = this.props; - const { controllerId, onlineStatus, sessionId, stakers, stashId } = this.state; - const bonded = stakers && !stakers.own.isEmpty - ? [stakers.own.unwrap(), stakers.total.unwrap().sub(stakers.own.unwrap())] - : true; - - if ((filter === 'hasNominators' && !this.hasNominators()) || - (filter === 'noNominators' && this.hasNominators()) || - (filter === 'hasWarnings' && !this.hasWarnings()) || - (filter === 'noWarnings' && this.hasWarnings()) || - (filter === 'iNominated' && !this.iNominated())) { - return null; + setOnlineStatus({ + hasOfflineWarnings: !!(stashId && onlineStatus.offline && onlineStatus.offline.length), + onlineStatus + }); } + }, [recentlyOnline, stakingInfo]); - const isAuthor = !!lastBlock && !!lastAuthor && [address, controllerId, stashId].includes(lastAuthor); - const nominators = this.getNominators(); - - return ( - - {isAuthor && ( -
#{lastBlock}
- )} - {controllerId && ( -
- - -
- )} - {!isSubstrateV2 && sessionId && ( -
- - -
- )} - - } - className={className} - defaultName={defaultName} - iconInfo={controllerId && onlineStatus && ( - - )} - key={stashId || controllerId || undefined} - value={stashId || address} - withBalance={{ bonded }} - withValidatorPrefs={{ validatorPayment: true }} - > - {nominators.length !== 0 && ( -
- - {t('Nominators ({{count}})', { - replace: { - count: nominators.length - } - })} - - {nominators.map(([who, bonded]): React.ReactNode => - - )} -
- )} -
- ); + if ((filter === 'hasNominators' && !hasNominators) || + (filter === 'noNominators' && hasNominators) || + (filter === 'hasWarnings' && !hasOfflineWarnings) || + (filter === 'noWarnings' && hasOfflineWarnings) || + (filter === 'iNominated' && !isNominatorMe)) { + return null; } - private getNominators (): [AccountId, Balance][] { - const { stakers } = this.state; + const isAuthor = !!lastBlock && !!lastAuthor && [address, controllerId, stashId].includes(lastAuthor); - return stakers - ? stakers.others.map(({ who, value }): [AccountId, Balance] => [who, value.unwrap()]) - : []; - } - - private iNominated (): boolean { - const nominators = this.getNominators(); - const myAddresses = keyring.getAccounts().map(({ address }): string => address); - - return nominators.some(([who]): boolean => - myAddresses.includes(who.toString()) - ); - } - - private hasNominators (): boolean { - const nominators = this.getNominators(); - - return !!nominators.length; - } - - private hasWarnings (): boolean { - const { stashId, onlineStatus } = this.state; - - if (!stashId || !onlineStatus.offline || !onlineStatus.offline.length) { - return false; - } - - return true; - } + return ( + + {isAuthor && ( +
#{lastBlock}
+ )} + {controllerId && ( +
+ + +
+ )} + {!isSubstrateV2 && sessionId && ( +
+ + +
+ )} + + } + className={className} + defaultName={defaultName} + iconInfo={controllerId && onlineStatus && ( + + )} + key={stashId || controllerId || undefined} + value={stashId || address} + withBalance={balanceOpts} + withValidatorPrefs={WITH_VALIDATOR_PREFS} + > + {hasNominators && ( +
+ + {t('Nominators ({{count}})', { + replace: { + count: nominators.length + } + })} + + {nominators.map(([who, bonded]): React.ReactNode => + + )} +
+ )} +
+ ); } export default withMulti( - styled(Address as React.ComponentClass)` + styled(Address)` .blockNumberV1, .blockNumberV2 { background: #3f3f3f; @@ -235,6 +219,9 @@ export default withMulti( `, translate, withCalls( - ['derive.staking.info', { paramName: 'address' }] + ['derive.staking.info', { + paramName: 'address', + propName: 'stakingInfo' + }] ) ); diff --git a/packages/app-staking/src/index.tsx b/packages/app-staking/src/index.tsx index c202a762e0..66d65859d0 100644 --- a/packages/app-staking/src/index.tsx +++ b/packages/app-staking/src/index.tsx @@ -2,17 +2,16 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { AccountId, AuthorityId, BlockNumber, EventRecord } from '@polkadot/types/interfaces'; +import { AccountId, BlockNumber, EventRecord } from '@polkadot/types/interfaces'; import { AppProps, I18nProps } from '@polkadot/react-components/types'; import { ApiProps } from '@polkadot/react-api/types'; import { SubjectInfo } from '@polkadot/ui-keyring/observable/types'; import { ComponentProps } from './types'; -import BN from 'bn.js'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useReducer } from 'react'; import { Route, Switch } from 'react-router'; import styled from 'styled-components'; -import { createType, Option } from '@polkadot/types'; +import { Option } from '@polkadot/types'; import { HelpOverlay } from '@polkadot/react-components'; import Tabs from '@polkadot/react-components/Tabs'; import { withCalls, withMulti, withObservable } from '@polkadot/react-api'; @@ -25,55 +24,37 @@ import translate from './translate'; interface Props extends AppProps, ApiProps, I18nProps { allAccounts?: SubjectInfo; - allStashesAndControllers?: [AccountId[], Option[]]; + allStashesAndControllers?: [string[], string[]]; bestNumber?: BlockNumber; - currentValidatorsControllersV1OrStashesV2?: AccountId[]; - recentlyOnline?: AuthorityId[]; + currentValidators?: string[]; + recentlyOnline?: string[]; } -interface State { - allControllers: string[]; - allStashes: string[]; - currentValidators: string[]; +const EMPY_ACCOUNTS: string[] = []; +const EMPTY_ALL: [string[], string[]] = [EMPY_ACCOUNTS, EMPY_ACCOUNTS]; + +function offlineReducer (prev: Record, { bestNumber, recentlyOnline }: { bestNumber: BlockNumber; recentlyOnline: string[] }): Record { + return { + ...prev, + ...recentlyOnline.reduce( + (result: Record, authorityId): Record => ({ + ...result, + [authorityId]: bestNumber + }), + {} + ) + }; } -function App ({ allAccounts, allStashesAndControllers, bestNumber, className, currentValidatorsControllersV1OrStashesV2, basePath, recentlyOnline: propsRecentlyOnline, t }: Props): React.ReactElement { - const [{ allControllers, allStashes, currentValidators }, setState] = useState({ - allControllers: [], - allStashes: [], - currentValidators: [] - }); - const [recentlyOnline, setRecentlyOnline] = useState>({}); +function App ({ allAccounts, allStashesAndControllers: [allStashes, allControllers] = EMPTY_ALL, bestNumber, className, currentValidators = EMPY_ACCOUNTS, basePath, recentlyOnline, t }: Props): React.ReactElement { + const [online, dispatchOffline] = useReducer(offlineReducer, {}); + // dispatch a combinator for the new recentlyOnline events useEffect((): void => { - const [_stashes, _controllers] = (allStashesAndControllers || [[], []]); - const _validators = currentValidatorsControllersV1OrStashesV2 || []; - - setState({ - allControllers: _controllers - .filter((optId): boolean => optId.isSome) - .map((accountId): string => accountId.unwrap().toString()), - allStashes: _stashes - .filter((): boolean => true) - .map((accountId): string => accountId.toString()), - currentValidators: _validators.map((authorityId): string => - authorityId.toString() - ) - }); - }, [allStashesAndControllers, currentValidatorsControllersV1OrStashesV2]); - - useEffect((): void => { - setRecentlyOnline({ - ...(recentlyOnline || {}), - ...(propsRecentlyOnline || []).reduce( - (result: Record, authorityId): Record => ({ - ...result, - [authorityId.toString()]: bestNumber || createType('BlockNumber', new BN(0)) - }), - {} - ) - }); - }, [bestNumber, propsRecentlyOnline]); + if (bestNumber && recentlyOnline && recentlyOnline.length) { + dispatchOffline({ bestNumber, recentlyOnline }); + } + }, [bestNumber, recentlyOnline]); const _renderComponent = (Component: React.ComponentType): () => React.ReactNode => { // eslint-disable-next-line react/display-name @@ -88,7 +69,7 @@ function App ({ allAccounts, allStashesAndControllers, bestNumber, className, cu allControllers={allControllers} allStashes={allStashes} currentValidators={currentValidators} - recentlyOnline={recentlyOnline} + recentlyOnline={online} /> ); }; @@ -135,17 +116,29 @@ export default withMulti( translate, withCalls( ['derive.chain.bestNumber', { propName: 'bestNumber' }], - ['derive.staking.controllers', { propName: 'allStashesAndControllers' }], - ['query.session.validators', { propName: 'currentValidatorsControllersV1OrStashesV2' }], + ['derive.staking.controllers', { + propName: 'allStashesAndControllers', + transform: ([stashes, controllers]: [AccountId[], Option[]]): [string[], string[]] => [ + stashes.map((accountId): string => accountId.toString()), + controllers + .filter((optId): boolean => optId.isSome) + .map((accountId): string => accountId.unwrap().toString()) + ] + }], + ['query.session.validators', { + propName: 'currentValidators', + transform: (validators: AccountId[]): string[] => + validators.map((accountId): string => accountId.toString()) + }], ['query.system.events', { propName: 'recentlyOnline', - transform: (value?: EventRecord[]): AuthorityId[] => + transform: (value?: EventRecord[]): string[] => (value || []) .filter(({ event: { method, section } }): boolean => section === 'imOnline' && method === 'HeartbeatReceived' ) - .map(({ event: { data: [authorityId] } }): AuthorityId => - authorityId as AuthorityId + .map(({ event: { data: [authorityId] } }): string => + authorityId.toString() ) }] ),