Hooks reworks with mounted refs (#2417)
This commit is contained in:
parent
e8f6da1041
commit
b720cd1443
@ -16,13 +16,13 @@ export default function useAvailableSlashes (): [BN, UnappliedSlash[]][] {
|
||||
const { api } = useApi();
|
||||
const indexes = useCall<DeriveSessionIndexes>(api.derive.session?.indexes, []);
|
||||
const earliestSlash = useCall<Option<EraIndex>>(api.query.staking?.earliestUnappliedSlash, []);
|
||||
const mounted = useIsMountedRef();
|
||||
const mountedRef = useIsMountedRef();
|
||||
const [slashes, setSlashes] = useState<[BN, UnappliedSlash[]][]>([]);
|
||||
|
||||
useEffect((): Unsub => {
|
||||
let unsub: Unsub | undefined;
|
||||
|
||||
if (mounted.current && indexes && earliestSlash?.isSome) {
|
||||
if (mountedRef.current && indexes && earliestSlash?.isSome) {
|
||||
const from = earliestSlash.unwrap();
|
||||
const range: BN[] = [];
|
||||
let start = new BN(from);
|
||||
@ -34,13 +34,13 @@ export default function useAvailableSlashes (): [BN, UnappliedSlash[]][] {
|
||||
|
||||
if (range.length) {
|
||||
(async (): Promise<void> => {
|
||||
unsub = await api.query.staking.unappliedSlashes.multi<Vec<UnappliedSlash>>(range, (values): void =>
|
||||
setSlashes(
|
||||
unsub = await api.query.staking.unappliedSlashes.multi<Vec<UnappliedSlash>>(range, (values): void => {
|
||||
mountedRef.current && setSlashes(
|
||||
values
|
||||
.map((value, index): [BN, UnappliedSlash[]] => [from.addn(index), value])
|
||||
.filter(([, slashes]): boolean => slashes.length !== 0)
|
||||
)
|
||||
);
|
||||
);
|
||||
});
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,30 +4,29 @@
|
||||
|
||||
import { AccountId, Balance } from '@polkadot/types/interfaces';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAccounts, useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
interface Result {
|
||||
isMember: boolean;
|
||||
members: string[];
|
||||
}
|
||||
function transform (allAccounts: string[]): (queryMembers: [AccountId, Balance][]) => Result {
|
||||
return (queryMembers: [AccountId, Balance][]): Result => {
|
||||
const members = queryMembers.map((accountId): string => accountId.toString());
|
||||
|
||||
return {
|
||||
members,
|
||||
isMember: members.some((accountId): boolean => allAccounts.includes(accountId))
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default function useCouncilMembers (): Result {
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
const queryMembers = useCall<[AccountId, Balance][]>(api.query.council.members, []);
|
||||
const [members, setMembers] = useState<Result>({ isMember: false, members: [] });
|
||||
|
||||
useEffect((): void => {
|
||||
if (allAccounts && queryMembers) {
|
||||
const members = queryMembers.map((accountId): string => accountId.toString());
|
||||
|
||||
setMembers({
|
||||
members,
|
||||
isMember: members.some((accountId): boolean => allAccounts.includes(accountId))
|
||||
});
|
||||
}
|
||||
}, [allAccounts, queryMembers]);
|
||||
const members = useCall<Result>(api.query.council.members, [], {
|
||||
transform: transform(allAccounts)
|
||||
}) || { isMember: false, members: [] };
|
||||
|
||||
return members;
|
||||
}
|
||||
|
||||
@ -4,21 +4,17 @@
|
||||
|
||||
import { DerivedCollectiveProposals } from '@polkadot/api-derive/types';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
function transform (motions: DerivedCollectiveProposals): number {
|
||||
return motions.filter(({ votes }): boolean => !!votes).length;
|
||||
}
|
||||
|
||||
export default function useCounter (): number {
|
||||
const { api, isApiReady } = useApi();
|
||||
const motions = useCall<DerivedCollectiveProposals>(isApiReady && api.derive.council?.proposals, []);
|
||||
const [counter, setCounter] = useState(0);
|
||||
|
||||
useEffect((): void => {
|
||||
setCounter(
|
||||
motions
|
||||
? motions.filter(({ votes }): boolean => !!votes).length
|
||||
: 0
|
||||
);
|
||||
}, [motions]);
|
||||
const counter = useCall<number>(isApiReady && api.derive.council?.proposals, [], {
|
||||
transform
|
||||
}) || 0;
|
||||
|
||||
return counter;
|
||||
}
|
||||
|
||||
@ -3,16 +3,17 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { useApi, useCall, useIsMountedRef } from '@polkadot/react-hooks';
|
||||
|
||||
export default function useCounter (): number {
|
||||
const { api, isApiReady } = useApi();
|
||||
const mountedRef = useIsMountedRef();
|
||||
const proposals = useCall<any[]>(isApiReady && api.derive.democracy?.proposals, []);
|
||||
const referenda = useCall<any[]>(isApiReady && api.derive.democracy?.referendums, []);
|
||||
const [counter, setCounter] = useState(0);
|
||||
|
||||
useEffect((): void => {
|
||||
setCounter(
|
||||
mountedRef.current && setCounter(
|
||||
(proposals?.length || 0) +
|
||||
(referenda?.length || 0)
|
||||
);
|
||||
|
||||
@ -5,26 +5,26 @@
|
||||
import { DeriveSocietyMember } from '@polkadot/api-derive/types';
|
||||
import { OwnMembers } from './types';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAccounts, useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
function transform (allAccounts: string[]): (members: DeriveSocietyMember[]) => OwnMembers {
|
||||
return (members: DeriveSocietyMember[]): OwnMembers => {
|
||||
const allMembers = members
|
||||
.filter((member): boolean => !member.isSuspended)
|
||||
.map((member): string => member.accountId.toString());
|
||||
const ownMembers = allMembers
|
||||
.filter((address): boolean => allAccounts.includes(address));
|
||||
|
||||
return { allMembers, isMember: ownMembers.length !== 0, ownMembers };
|
||||
};
|
||||
}
|
||||
|
||||
export default function useMembers (): OwnMembers {
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
const members = useCall<DeriveSocietyMember[]>(api.derive.society?.members, []);
|
||||
const [ownState, setOwnState] = useState<OwnMembers>({ allMembers: [], isMember: false, ownMembers: [] });
|
||||
const state = useCall<OwnMembers>(api.derive.society?.members, [], {
|
||||
transform: transform(allAccounts)
|
||||
}) || { allMembers: [], isMember: false, ownMembers: [] };
|
||||
|
||||
useEffect((): void => {
|
||||
if (allAccounts && members) {
|
||||
const allMembers = members
|
||||
.filter((member): boolean => !member.isSuspended)
|
||||
.map((member): string => member.accountId.toString());
|
||||
const ownMembers = allMembers
|
||||
.filter((address): boolean => allAccounts.includes(address));
|
||||
|
||||
setOwnState({ allMembers, isMember: ownMembers.length !== 0, ownMembers });
|
||||
}
|
||||
}, [allAccounts, members]);
|
||||
|
||||
return ownState;
|
||||
return state;
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ export default function useInactives (stashId: string, nominees?: string[]): str
|
||||
}
|
||||
});
|
||||
|
||||
setInactives(inactives);
|
||||
mountedRef.current && setInactives(inactives);
|
||||
}
|
||||
)
|
||||
.then((_unsub): void => {
|
||||
|
||||
@ -22,15 +22,14 @@ interface Props {
|
||||
defaultName: string;
|
||||
filterName: string;
|
||||
hasQueries: boolean;
|
||||
heartbeat?: DerivedHeartbeatAuthor;
|
||||
heartbeat?: false | DerivedHeartbeatAuthor;
|
||||
isAuthor?: boolean;
|
||||
isElected: boolean;
|
||||
isFavorite: boolean;
|
||||
isMain?: boolean;
|
||||
lastBlock?: string;
|
||||
myAccounts: string[];
|
||||
points?: RewardPoint;
|
||||
setNominators?: (nominators: string[]) => void;
|
||||
points?: false | RewardPoint;
|
||||
setNominators?: false | ((nominators: string[]) => void);
|
||||
toggleFavorite: (accountId: string) => void;
|
||||
withNominations?: boolean;
|
||||
}
|
||||
@ -45,7 +44,7 @@ interface StakingState {
|
||||
stakeOwn?: BN;
|
||||
}
|
||||
|
||||
function expandInfo ({ controllerId, exposure, nextSessionIds, validatorPrefs }: DerivedStakingQuery, myAccounts: string[], withNominations = true): StakingState {
|
||||
function expandInfo ({ controllerId, exposure, nextSessionIds, validatorPrefs }: DerivedStakingQuery, withNominations = true): StakingState {
|
||||
const nominators = withNominations && exposure
|
||||
? exposure.others.map(({ who, value }): [AccountId, Balance] => [who, value.unwrap()])
|
||||
: [];
|
||||
@ -98,7 +97,7 @@ function checkVisibility (api: ApiPromise, address: string, filterName: string,
|
||||
return isVisible;
|
||||
}
|
||||
|
||||
function Address ({ address, className, filterName, hasQueries, heartbeat, isAuthor, isElected, isFavorite, isMain, lastBlock, myAccounts, points, setNominators, toggleFavorite, withNominations }: Props): React.ReactElement<Props> | null {
|
||||
function Address ({ address, className, filterName, hasQueries, heartbeat, isAuthor, isElected, isFavorite, isMain, lastBlock, points, setNominators, toggleFavorite, withNominations }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const info = useCall<DeriveAccountInfo>(api.derive.accounts.info as any, [address]);
|
||||
@ -109,7 +108,7 @@ function Address ({ address, className, filterName, hasQueries, heartbeat, isAut
|
||||
|
||||
useEffect((): void => {
|
||||
if (stakingInfo) {
|
||||
const info = expandInfo(stakingInfo, myAccounts, withNominations);
|
||||
const info = expandInfo(stakingInfo, withNominations);
|
||||
|
||||
setNominators && setNominators(info.nominators.map(([who]): string => who.toString()));
|
||||
setStakingState(info);
|
||||
@ -220,7 +219,7 @@ function Address ({ address, className, filterName, hasQueries, heartbeat, isAut
|
||||
)}
|
||||
</td>
|
||||
<td className='number'>
|
||||
{points?.gtn(0) && (
|
||||
{points && points.gtn(0) && (
|
||||
<><label>{t('points')}</label>{formatNumber(points)}</>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@ -8,7 +8,7 @@ import { AddressDetails } from './types';
|
||||
|
||||
import React, { useEffect, useReducer, useState } from 'react';
|
||||
import { Input, Spinner, Table } from '@polkadot/react-components';
|
||||
import { useAccounts, useFavorites } from '@polkadot/react-hooks';
|
||||
import { useFavorites } from '@polkadot/react-hooks';
|
||||
|
||||
import { STORE_FAVS_BASE } from '../constants';
|
||||
import { useTranslation } from '../translate';
|
||||
@ -28,10 +28,10 @@ interface Props {
|
||||
|
||||
type AccountExtend = [string, boolean, boolean];
|
||||
|
||||
function sortByFav ([,, isFavA]: AccountExtend, [,, isFavB]: AccountExtend): number {
|
||||
return isFavA === isFavB
|
||||
? 0
|
||||
: (isFavA ? -1 : 1);
|
||||
interface Filtered {
|
||||
elected: AccountExtend[];
|
||||
validators: AccountExtend[];
|
||||
waiting?: AccountExtend[];
|
||||
}
|
||||
|
||||
function filterAccounts (accounts: string[] = [], elected: string[], favorites: string[], without: string[]): AccountExtend[] {
|
||||
@ -42,7 +42,11 @@ function filterAccounts (accounts: string[] = [], elected: string[], favorites:
|
||||
elected.includes(accountId),
|
||||
favorites.includes(accountId)
|
||||
])
|
||||
.sort(sortByFav);
|
||||
.sort(([,, isFavA]: AccountExtend, [,, isFavB]: AccountExtend): number =>
|
||||
isFavA === isFavB
|
||||
? 0
|
||||
: (isFavA ? -1 : 1)
|
||||
);
|
||||
}
|
||||
|
||||
function accountsToString (accounts: AccountId[]): string[] {
|
||||
@ -64,48 +68,53 @@ function reduceDetails (state: Record<string, AddressDetails>, _details: Address
|
||||
}, { ...state });
|
||||
}
|
||||
|
||||
function getDetails (stakingOverview: DerivedStakingOverview, validators: AccountExtend[]): AddressDetails[] {
|
||||
const allPoints = [...stakingOverview.eraPoints.individual.entries()];
|
||||
|
||||
return validators.map(([address]): AddressDetails => {
|
||||
const points = allPoints.find(([accountId]): boolean => accountId.eq(address));
|
||||
|
||||
return {
|
||||
address,
|
||||
points: points
|
||||
? points[1]
|
||||
: undefined
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getFiltered (stakingOverview: DerivedStakingOverview, favorites: string[], next?: string[]): Filtered {
|
||||
const allElected = accountsToString(stakingOverview.nextElected);
|
||||
const validatorIds = accountsToString(stakingOverview.validators);
|
||||
const validators = filterAccounts(validatorIds, allElected, favorites, []);
|
||||
const elected = filterAccounts(allElected, allElected, favorites, validatorIds);
|
||||
const waiting = filterAccounts(next, [], favorites, allElected);
|
||||
|
||||
return {
|
||||
elected,
|
||||
validators,
|
||||
waiting
|
||||
};
|
||||
}
|
||||
|
||||
function CurrentList ({ authorsMap, hasQueries, isIntentions, isVisible, lastAuthors, next, recentlyOnline, setNominators, stakingOverview }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { allAccounts } = useAccounts();
|
||||
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE);
|
||||
const [{ allElected, elected, validators, waiting }, setFiltered] = useState<{ allElected: string[]; elected: AccountExtend[]; validators: AccountExtend[]; waiting?: AccountExtend[] }>({ allElected: [], elected: [], validators: [] });
|
||||
const [{ elected, validators, waiting }, setFiltered] = useState<Filtered>({ elected: [], validators: [] });
|
||||
const [nameFilter, setNameFilter] = useState<string>('');
|
||||
const [addressDetails, dispatchDetails] = useReducer(reduceDetails, {});
|
||||
|
||||
useEffect((): void => {
|
||||
if (isVisible && stakingOverview) {
|
||||
const allElected = accountsToString(stakingOverview.nextElected);
|
||||
const _validators = accountsToString(stakingOverview.validators);
|
||||
const validators = filterAccounts(_validators, allElected, favorites, []);
|
||||
const elected = filterAccounts(allElected, allElected, favorites, _validators);
|
||||
|
||||
setFiltered({
|
||||
allElected,
|
||||
elected,
|
||||
validators,
|
||||
waiting: filterAccounts(next, [], favorites, allElected)
|
||||
});
|
||||
}
|
||||
}, [favorites, isVisible, next, stakingOverview?.nextElected, stakingOverview?.validators]);
|
||||
isVisible && stakingOverview && setFiltered(
|
||||
getFiltered(stakingOverview, favorites, next)
|
||||
);
|
||||
}, [favorites, isVisible, next, stakingOverview]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (stakingOverview?.eraPoints) {
|
||||
const allPoints = stakingOverview.eraPoints
|
||||
? [...stakingOverview.eraPoints.individual.entries()]
|
||||
: [];
|
||||
|
||||
dispatchDetails(validators.map(([address]): AddressDetails => {
|
||||
const points = allPoints.find(([accountId]): boolean => accountId.eq(address));
|
||||
|
||||
return {
|
||||
address,
|
||||
points: points
|
||||
? points[1]
|
||||
: undefined
|
||||
};
|
||||
}));
|
||||
}
|
||||
}, [stakingOverview?.eraPoints, allElected, validators]);
|
||||
stakingOverview && dispatchDetails(
|
||||
getDetails(stakingOverview, validators)
|
||||
);
|
||||
}, [stakingOverview, validators]);
|
||||
|
||||
const _renderRows = (addresses: AccountExtend[], defaultName: string, isMain: boolean): React.ReactNode =>
|
||||
addresses.map(([address, isElected, isFavorite]): React.ReactNode => (
|
||||
@ -114,24 +123,15 @@ function CurrentList ({ authorsMap, hasQueries, isIntentions, isVisible, lastAut
|
||||
defaultName={defaultName}
|
||||
filterName={nameFilter}
|
||||
hasQueries={hasQueries}
|
||||
heartbeat={
|
||||
isMain && recentlyOnline
|
||||
? recentlyOnline[address]
|
||||
: undefined
|
||||
}
|
||||
heartbeat={isMain && recentlyOnline && recentlyOnline[address]}
|
||||
isAuthor={lastAuthors && lastAuthors.includes(address)}
|
||||
isElected={isElected}
|
||||
isFavorite={isFavorite}
|
||||
isMain={isMain}
|
||||
key={address}
|
||||
lastBlock={authorsMap[address]}
|
||||
myAccounts={allAccounts}
|
||||
points={
|
||||
isMain
|
||||
? addressDetails[address] && addressDetails[address].points
|
||||
: undefined
|
||||
}
|
||||
setNominators={isIntentions ? undefined : setNominators}
|
||||
points={isMain && addressDetails[address] && addressDetails[address].points}
|
||||
setNominators={isIntentions && setNominators}
|
||||
toggleFavorite={toggleFavorite}
|
||||
/>
|
||||
));
|
||||
|
||||
@ -1,271 +0,0 @@
|
||||
// Copyright 2017-2020 @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 { Balance, Hash, Exposure } from '@polkadot/types/interfaces';
|
||||
import { SessionRewards, Slash } from '../types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Chart, Columar, Column } from '@polkadot/react-components';
|
||||
import { toShortAddress } from '@polkadot/react-components/util';
|
||||
import { getHistoric } from '@polkadot/react-api/util';
|
||||
import { useApi } from '@polkadot/react-hooks';
|
||||
import { formatBalance, formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import useBlockCounts from './useBlockCounts';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
sessionRewards: SessionRewards[];
|
||||
validatorId: string;
|
||||
}
|
||||
|
||||
type LineDataEntry = (BN | number)[];
|
||||
|
||||
type LineData = LineDataEntry[];
|
||||
|
||||
interface SplitEntry {
|
||||
colors: string[];
|
||||
label: string;
|
||||
tooltip: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
type SplitData = SplitEntry[];
|
||||
|
||||
const COLORS_MINE = ['#ff8c00'];
|
||||
const COLORS_OTHER = ['#acacac'];
|
||||
const COLORS_REWARD = ['#8c2200', '#008c22', '#acacac'];
|
||||
const COLORS_BLOCKS = [undefined, '#acacac'];
|
||||
|
||||
function balanceToNumber (amount: BN, divisor: BN): number {
|
||||
return amount.muln(1000).div(divisor).toNumber() / 1000;
|
||||
}
|
||||
|
||||
function extractStake (values: [Hash, Exposure][], divisor: BN): LineData {
|
||||
return [
|
||||
values.map(([, { total }]): number =>
|
||||
balanceToNumber(total.unwrap(), divisor))
|
||||
// exposures.map(({ own }): number =>
|
||||
// balanceToNumber(own.unwrap(), divisor)),
|
||||
// exposures.map(({ others }): number =>
|
||||
// balanceToNumber(
|
||||
// others.reduce((total, { value }): number =>
|
||||
// total.add(value.unwrap()), new BN(0)
|
||||
// ), divisor))
|
||||
];
|
||||
}
|
||||
|
||||
function extractSplit (values: [Hash, Exposure][], validatorId: string): SplitData | null {
|
||||
if (!values.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const last = values[values.length - 1][1];
|
||||
const total = last.total.unwrap();
|
||||
|
||||
if (total.eqn(0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currency = formatBalance.getDefaults().unit;
|
||||
|
||||
return [{ accountId: validatorId, isOwn: true, value: last.own.unwrap() }]
|
||||
.concat(last.others.map(({ who, value }): { accountId: string; isOwn: boolean; value: Balance } => ({
|
||||
accountId: who.toString(), isOwn: false, value: value.unwrap()
|
||||
})))
|
||||
.sort((a, b): number => b.value.cmp(a.value))
|
||||
.map(({ accountId, isOwn, value }): SplitEntry => {
|
||||
const label = toShortAddress(accountId);
|
||||
const percentage = value.muln(10000).div(total).toNumber() / 100;
|
||||
const tooltip = `${formatBalance(value, { forceUnit: '-', withSi: false })} ${currency} (${percentage.toFixed(2)}%)`;
|
||||
|
||||
return {
|
||||
colors: isOwn ? COLORS_MINE : COLORS_OTHER,
|
||||
label,
|
||||
tooltip,
|
||||
value: percentage
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function extractEraSlash (validatorId: string, slashes: Slash[]): BN {
|
||||
return slashes.reduce((total: BN, { accountId, amount }): BN => {
|
||||
return accountId.eq(validatorId)
|
||||
? total.sub(amount)
|
||||
: total;
|
||||
}, new BN(0));
|
||||
}
|
||||
|
||||
export default function Validator ({ className, sessionRewards, validatorId }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const blockCounts = useBlockCounts(validatorId, sessionRewards);
|
||||
const [blocksLabels, setBlocksLabels] = useState<string[]>([]);
|
||||
const [blocksChart, setBlocksChart] = useState<LineData | null>(null);
|
||||
const [{ currency, divisor }] = useState<{ currency: string; divisor: BN }>({
|
||||
currency: formatBalance.getDefaults().unit,
|
||||
divisor: new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0'))
|
||||
});
|
||||
const [{ rewardsChart, rewardsLabels }, setRewardsInfo] = useState<{ rewardsChart: LineData | null; rewardsLabels: string[] }>({ rewardsChart: null, rewardsLabels: [] });
|
||||
const [{ splitChart, splitMax }, setSplitInfo] = useState<{ splitChart: SplitData | null; splitMax: number }>({ splitChart: null, splitMax: 100 });
|
||||
const [{ stakeChart, stakeLabels }, setStakeInfo] = useState<{ stakeChart: LineData | null; stakeLabels: string[]}>({ stakeChart: null, stakeLabels: [] });
|
||||
|
||||
useEffect((): void => {
|
||||
if (!splitChart) {
|
||||
const hashes = sessionRewards.map(({ blockHash }): Hash => blockHash);
|
||||
const stakeLabels = sessionRewards.map(({ sessionIndex }): string => formatNumber(sessionIndex));
|
||||
|
||||
api.isReady.then(async (): Promise<void> => {
|
||||
const values = await getHistoric<Exposure>(api.query.staking.stakers.at, [validatorId], hashes);
|
||||
const stakeChart = extractStake(values, divisor);
|
||||
const splitChart = extractSplit(values, validatorId);
|
||||
const splitMax = splitChart ? Math.min(Math.ceil(splitChart[0].value), 100) : 100;
|
||||
|
||||
setStakeInfo({ stakeChart, stakeLabels });
|
||||
setSplitInfo({ splitChart, splitMax });
|
||||
});
|
||||
}
|
||||
}, [sessionRewards, splitChart]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (blockCounts) {
|
||||
const rewardsLabels: string[] = [];
|
||||
const rewardsChart: LineData = [[], [], []];
|
||||
let total = new BN(0);
|
||||
let lastRewardIndex = 0;
|
||||
let rewardCount = 0;
|
||||
|
||||
// we only work from the second position, the first deemed incomplete
|
||||
sessionRewards.forEach(({ blockNumber, reward, sessionIndex, slashes }, index): void => {
|
||||
// we are trying to find the first index where rewards are allocated, this is our start
|
||||
// since we only want complete eras, this means we drop some at the start
|
||||
if (lastRewardIndex === 0) {
|
||||
if (index && reward.gtn(0)) {
|
||||
lastRewardIndex = index;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// slash is extracted from the available slashes
|
||||
const neg = extractEraSlash(validatorId, slashes);
|
||||
|
||||
// start of a new session, use the counts for the previous
|
||||
const totalBlocks = blockCounts
|
||||
.filter((count, countIndex): boolean =>
|
||||
!!count && countIndex >= lastRewardIndex && countIndex < index)
|
||||
.reduce((total, count): BN => total.add(count), new BN(0));
|
||||
|
||||
// calculate the rewards based on our total share
|
||||
const pos = reward
|
||||
.mul(totalBlocks)
|
||||
.div(blockNumber.sub(sessionRewards[lastRewardIndex].blockNumber));
|
||||
|
||||
// add this to the total
|
||||
total = total.add(neg).add(pos);
|
||||
rewardCount++;
|
||||
|
||||
// if we have a reward here, set the reward index for the next iteration
|
||||
if (reward.gtn(0)) {
|
||||
lastRewardIndex = index;
|
||||
}
|
||||
|
||||
// this shows the start of the new era, however rewards are for previous
|
||||
rewardsLabels.push(formatNumber(sessionIndex.subn(1)));
|
||||
|
||||
// calculate and format to 3 decimals
|
||||
rewardsChart[0].push(balanceToNumber(neg, divisor));
|
||||
rewardsChart[1].push(balanceToNumber(pos, divisor));
|
||||
rewardsChart[2].push(balanceToNumber(total.divn(rewardCount), divisor));
|
||||
});
|
||||
|
||||
setRewardsInfo({ rewardsChart, rewardsLabels });
|
||||
}
|
||||
}, [blockCounts, sessionRewards, validatorId]);
|
||||
|
||||
useEffect((): void => {
|
||||
setBlocksLabels(
|
||||
sessionRewards.map(({ sessionIndex }): string => formatNumber(sessionIndex))
|
||||
);
|
||||
}, [sessionRewards]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (blockCounts) {
|
||||
const avgSet: number[] = [];
|
||||
const idxSet: BN[] = [];
|
||||
|
||||
blockCounts.reduce((total: BN, value, index): BN => {
|
||||
total = total.add(value);
|
||||
|
||||
avgSet.push(total.muln(100).divn(index + 1).toNumber() / 100);
|
||||
idxSet.push(value);
|
||||
|
||||
return total;
|
||||
}, new BN(0));
|
||||
|
||||
setBlocksChart([idxSet, avgSet]);
|
||||
}
|
||||
}, [blockCounts]);
|
||||
|
||||
return (
|
||||
<Columar className={className}>
|
||||
<Column emptyText={t('Loading block data')}>
|
||||
{(rewardsChart || blocksChart) && (
|
||||
<>
|
||||
{blocksChart && (
|
||||
<div className='staking--Chart'>
|
||||
<h1>{t('blocks produced')}</h1>
|
||||
<Chart.Line
|
||||
colors={COLORS_BLOCKS}
|
||||
labels={blocksLabels}
|
||||
legends={[t('blocks'), t('average')]}
|
||||
values={blocksChart}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{rewardsChart && (
|
||||
<div className='staking--Chart'>
|
||||
<h1>{t('rewards & slashes')}</h1>
|
||||
<Chart.Line
|
||||
colors={COLORS_REWARD}
|
||||
labels={rewardsLabels}
|
||||
legends={[t('{{currency}} slashed', { replace: { currency } }), t('{{currency}} rewards (est.)', { replace: { currency } }), t('{{currency}} average', { replace: { currency } })]}
|
||||
values={rewardsChart}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Column>
|
||||
<Column emptyText={t('Loading staker data')}>
|
||||
{(stakeChart || splitChart) && (
|
||||
<>
|
||||
{stakeChart && stakeChart[0].length !== 0 && (
|
||||
<div className='staking--Chart'>
|
||||
<h1>{t('elected stake')}</h1>
|
||||
<Chart.Line
|
||||
labels={stakeLabels}
|
||||
legends={[t('{{currency}} total', { replace: { currency } }), t('{{currency}} own', { replace: { currency } }), t('{{currency}} other', { replace: { currency } })]}
|
||||
values={stakeChart}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{splitChart && (
|
||||
<div className='staking--Chart'>
|
||||
<h1>{t('staker percentages')}</h1>
|
||||
<Chart.HorizBar
|
||||
aspectRatio={2}
|
||||
max={splitMax}
|
||||
values={splitChart}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Column>
|
||||
</Columar>
|
||||
);
|
||||
}
|
||||
@ -11,7 +11,7 @@ import { u32 } from '@polkadot/types';
|
||||
|
||||
export default function useBlockCounts (accountId: string, sessionRewards: SessionRewards[]): u32[] {
|
||||
const { api } = useApi();
|
||||
const mounted = useIsMountedRef();
|
||||
const mountedRef = useIsMountedRef();
|
||||
const indexes = useCall<DeriveSessionIndexes>(api.derive.session?.indexes, []);
|
||||
const current = useCall<u32>(api.query.imOnline?.authoredBlocks, [indexes?.currentIndex, accountId]);
|
||||
const [counts, setCounts] = useState<u32[]>([]);
|
||||
@ -27,7 +27,7 @@ export default function useBlockCounts (accountId: string, sessionRewards: Sessi
|
||||
api.query.imOnline.authoredBlocks.at(parentHash, sessionIndex.subn(1), accountId) as Promise<u32>
|
||||
))
|
||||
.then((historic): void => {
|
||||
mounted.current && setHistoric(historic);
|
||||
mountedRef.current && setHistoric(historic);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,14 +26,14 @@ interface StakeInfo {
|
||||
function Summary ({ lastReward, numNominators, numValidators, totalStaked }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const totalInsurance = useCall<Balance>(api.query.balances.totalIssuance, []);
|
||||
const totalIssuance = useCall<Balance>(api.query.balances.totalIssuance, []);
|
||||
const [{ percentage }, setStakeInfo] = useState<StakeInfo>({});
|
||||
|
||||
useEffect((): void => {
|
||||
totalInsurance && totalStaked?.gtn(0) && setStakeInfo({
|
||||
percentage: `${(totalStaked.muln(10000).div(totalInsurance).toNumber() / 100).toFixed(2)}%`
|
||||
totalIssuance && totalStaked?.gtn(0) && setStakeInfo({
|
||||
percentage: `${(totalStaked.muln(10000).div(totalIssuance).toNumber() / 100).toFixed(2)}%`
|
||||
});
|
||||
}, [totalInsurance, totalStaked]);
|
||||
}, [totalIssuance, totalStaked]);
|
||||
|
||||
return (
|
||||
<SummaryBox>
|
||||
@ -46,13 +46,13 @@ function Summary ({ lastReward, numNominators, numValidators, totalStaked }: Pro
|
||||
/>
|
||||
</CardSummary>
|
||||
)}
|
||||
{totalStaked && totalInsurance && (
|
||||
{totalStaked && totalIssuance && (
|
||||
<CardSummary label=''>/</CardSummary>
|
||||
)}
|
||||
{totalStaked && (
|
||||
{totalIssuance && (
|
||||
<CardSummary label={t('total issuance')}>
|
||||
<FormatBalance
|
||||
value={totalInsurance}
|
||||
value={totalIssuance}
|
||||
withSi
|
||||
/>
|
||||
</CardSummary>
|
||||
|
||||
@ -19,7 +19,7 @@ import { useTranslation } from '../translate';
|
||||
import Summary from './Summary';
|
||||
import Validator from './Validator';
|
||||
|
||||
const PERBILL = new BN(1000000000);
|
||||
const PERBILL = new BN(1_000_000_000);
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -167,7 +167,7 @@ function Targets ({ className }: Props): React.ReactElement<Props> {
|
||||
transform: (optBalance: Option<Balance>) =>
|
||||
optBalance.unwrapOrDefault()
|
||||
});
|
||||
const [_amount, setAmount] = useState<BN | undefined>(new BN(1000));
|
||||
const [_amount, setAmount] = useState<BN | undefined>(new BN(1_000));
|
||||
const electedInfo = useCall<DerivedStakingElected>(api.derive.staking.electedInfo, []);
|
||||
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE);
|
||||
const [{ nominators, validators, totalStaked }, setWorkable] = useState<AllInfo>({ nominators: [], validators: [] });
|
||||
@ -190,9 +190,7 @@ function Targets ({ className }: Props): React.ReactElement<Props> {
|
||||
)
|
||||
.sort((a, b): number => a.isFavorite === b.isFavorite
|
||||
? 0
|
||||
: a.isFavorite
|
||||
? -1
|
||||
: 1
|
||||
: (a.isFavorite ? -1 : 1)
|
||||
)
|
||||
});
|
||||
},
|
||||
|
||||
@ -42,23 +42,17 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement<Props>
|
||||
const recentlyOnline = useCall<DerivedHeartbeats>(api.derive.imOnline?.receivedHeartbeats, []);
|
||||
const stakingOverview = useCall<DerivedStakingOverview>(api.derive.staking.overview, []);
|
||||
const [nominators, dispatchNominators] = useReducer(reduceNominators, [] as string[]);
|
||||
const hasQueries = useMemo((): boolean => {
|
||||
return hasAccounts && !!(api.query.imOnline?.authoredBlocks) && !!(api.query.staking.activeEra);
|
||||
}, [api, hasAccounts]);
|
||||
const hasQueries = useMemo(
|
||||
(): boolean =>
|
||||
hasAccounts && !!(api.query.imOnline?.authoredBlocks) && !!(api.query.staking.activeEra),
|
||||
[api, hasAccounts]
|
||||
);
|
||||
const items = useMemo(() => [
|
||||
{
|
||||
isRoot: true,
|
||||
name: 'overview',
|
||||
text: t('Staking overview')
|
||||
},
|
||||
{
|
||||
name: 'waiting',
|
||||
text: t('Waiting')
|
||||
},
|
||||
{
|
||||
name: 'returns',
|
||||
text: t('Returns')
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
text: t('Account actions{{count}}', {
|
||||
@ -69,29 +63,37 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement<Props>
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'waiting',
|
||||
text: t('Waiting')
|
||||
},
|
||||
{
|
||||
name: 'returns',
|
||||
text: t('Returns')
|
||||
},
|
||||
{
|
||||
hasParams: true,
|
||||
name: 'query',
|
||||
text: t('Validator stats')
|
||||
}
|
||||
], [rewardCount, t]);
|
||||
const hiddenTabs = useMemo((): string[] => {
|
||||
const result = next ? [] : ['waiting'];
|
||||
|
||||
if (!hasAccounts) {
|
||||
result.push('actions', 'query');
|
||||
} else if (!hasQueries) {
|
||||
result.push('returns', 'query');
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [hasAccounts, hasQueries, next]);
|
||||
const hiddenTabs = useMemo(
|
||||
(): string[] =>
|
||||
!hasAccounts
|
||||
? ['actions', 'query']
|
||||
: !hasQueries
|
||||
? ['returns', 'query']
|
||||
: [],
|
||||
[hasAccounts, hasQueries]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
allStashes && stakingOverview && setNext(
|
||||
allStashes.filter((address): boolean => !stakingOverview.validators.includes(address as any))
|
||||
allStashes.filter((address): boolean =>
|
||||
!stakingOverview.validators.includes(address as any)
|
||||
)
|
||||
);
|
||||
}, [allStashes, stakingOverview?.validators]);
|
||||
}, [allStashes, stakingOverview]);
|
||||
|
||||
return (
|
||||
<main className={`staking--App ${className}`}>
|
||||
|
||||
@ -4,17 +4,17 @@
|
||||
|
||||
import { Hash } from '@polkadot/types/interfaces';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
function transform (proposals: Hash[]): number {
|
||||
return proposals.length;
|
||||
}
|
||||
|
||||
export default function useCounter (): number {
|
||||
const { api, isApiReady } = useApi();
|
||||
const proposals = useCall<Hash[]>(isApiReady && api.query.technicalCommittee?.proposals, []);
|
||||
const [counter, setCounter] = useState(0);
|
||||
|
||||
useEffect((): void => {
|
||||
setCounter(proposals?.length || 0);
|
||||
}, [proposals]);
|
||||
const counter = useCall<number>(isApiReady && api.query.technicalCommittee?.proposals, [], {
|
||||
transform
|
||||
}) || 0;
|
||||
|
||||
return counter;
|
||||
}
|
||||
|
||||
@ -65,13 +65,13 @@ function unsubscribe (tracker: TrackerRef): void {
|
||||
}
|
||||
|
||||
// subscribe, trying to play nice with the browser threads
|
||||
function subscribe <T> (mounted: MountedRef, tracker: TrackerRef, fn: TrackFn | undefined, params: CallParams, setValue: (value: T) => void, { isSingle, transform = transformIdentity, withParams }: CallOptions<T>): void {
|
||||
function subscribe <T> (mountedRef: MountedRef, tracker: TrackerRef, fn: TrackFn | undefined, params: CallParams, setValue: (value: T) => void, { isSingle, transform = transformIdentity, withParams }: CallOptions<T>): void {
|
||||
const validParams = params.filter((p): boolean => !isUndefined(p));
|
||||
|
||||
unsubscribe(tracker);
|
||||
|
||||
setTimeout((): void => {
|
||||
if (mounted.current) {
|
||||
if (mountedRef.current) {
|
||||
if (fn && (!fn.meta || !fn.meta.type?.isDoubleMap || validParams.length === 2)) {
|
||||
// swap to acive mode and reset our count
|
||||
tracker.current.isActive = true;
|
||||
@ -82,10 +82,10 @@ function subscribe <T> (mounted: MountedRef, tracker: TrackerRef, fn: TrackFn |
|
||||
tracker.current.subscriber = fn(...params, (value: any): void => {
|
||||
// when we don't have an active sub, or single-shot, ignore (we use the isActive flag here
|
||||
// since .subscriber may not be set on immeditae callback)
|
||||
if (mounted.current && tracker.current.isActive && (!isSingle || !tracker.current.count)) {
|
||||
if (mountedRef.current && tracker.current.isActive && (!isSingle || !tracker.current.count)) {
|
||||
tracker.current.count++;
|
||||
|
||||
mounted.current && tracker.current.isActive && setValue(
|
||||
mountedRef.current && tracker.current.isActive && setValue(
|
||||
withParams
|
||||
? [params, transform(value)] as any
|
||||
: transform(value)
|
||||
@ -104,7 +104,7 @@ function subscribe <T> (mounted: MountedRef, tracker: TrackerRef, fn: TrackFn |
|
||||
// - has a callback to set the value
|
||||
// FIXME The typings here need some serious TLC
|
||||
export default function useCall <T> (fn: TrackFn | undefined | null | false, params: CallParams = [], options: CallOptions<T> = {}): T | undefined {
|
||||
const mounted = useIsMountedRef();
|
||||
const mountedRef = useIsMountedRef();
|
||||
const tracker = useRef<Tracker>({ isActive: false, count: 0, serialized: null, subscriber: null });
|
||||
const [value, setValue] = useState<T | undefined>(options.defaultValue);
|
||||
|
||||
@ -116,13 +116,13 @@ export default function useCall <T> (fn: TrackFn | undefined | null | false, par
|
||||
// on changes, re-subscribe
|
||||
useEffect((): void => {
|
||||
// check if we have a function & that we are mounted
|
||||
if (mounted.current && fn) {
|
||||
if (mountedRef.current && fn) {
|
||||
const [serialized, mappedParams] = extractParams(fn, params, options.paramMap || transformIdentity);
|
||||
|
||||
if (mappedParams && serialized !== tracker.current.serialized) {
|
||||
tracker.current.serialized = serialized;
|
||||
|
||||
subscribe(mounted, tracker, fn, mappedParams, setValue, options);
|
||||
subscribe(mountedRef, tracker, fn, mappedParams, setValue, options);
|
||||
}
|
||||
}
|
||||
}, [fn, params]);
|
||||
|
||||
@ -8,12 +8,12 @@ import useIsMountedRef from './useIsMountedRef';
|
||||
|
||||
// Debounces inputs
|
||||
export default function useDebounce <T> (value: T, delay = 250): T {
|
||||
const mounted = useIsMountedRef();
|
||||
const mountedRef = useIsMountedRef();
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect((): () => void => {
|
||||
const handler = setTimeout(() => {
|
||||
mounted.current && setDebouncedValue(value);
|
||||
mountedRef.current && setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
// each time it renders, it clears
|
||||
|
||||
@ -21,7 +21,7 @@ function getResult (allAccounts: string[], members: string[]): Result {
|
||||
export default function useMembers (collective: 'council' | 'technicalCommittee'): Result {
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
const { isMember, members } = (
|
||||
const state = (
|
||||
collective === 'council'
|
||||
? useCall<Result>(api.query.electionsPhragmen?.members || api.query.elections.members, [], {
|
||||
transform: (accounts: [AccountId, Balance][]): Result =>
|
||||
@ -33,5 +33,5 @@ export default function useMembers (collective: 'council' | 'technicalCommittee'
|
||||
})
|
||||
) || { isMember: false, members: [] };
|
||||
|
||||
return { isMember, members };
|
||||
return state;
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import { useEffect, useState } from 'react';
|
||||
import BN from 'bn.js';
|
||||
import useApi from './useApi';
|
||||
import useCall from './useCall';
|
||||
import useIsMountedRef from './useIsMountedRef';
|
||||
import { useOwnStashIds } from './useOwnStashes';
|
||||
|
||||
interface OwnRewards {
|
||||
@ -19,13 +20,14 @@ interface OwnRewards {
|
||||
|
||||
function useNextPayouts (onlyLatest?: boolean): [string, BN][] | undefined {
|
||||
const { api, isApiReady } = useApi();
|
||||
const mountedRef = useIsMountedRef();
|
||||
const stashIds = useOwnStashIds();
|
||||
const allInfo = useCall<DerivedStakingQuery[]>(isApiReady && stashIds && api.derive.staking?.queryMulti, stashIds);
|
||||
const indexes = useCall<DeriveSessionIndexes>(isApiReady && api.derive.session?.indexes, []);
|
||||
const [nextPayouts, setNextPayouts] = useState<[string, BN][] | undefined>();
|
||||
|
||||
useEffect((): void => {
|
||||
if (stashIds && allInfo && indexes) {
|
||||
if (mountedRef.current && stashIds && allInfo && indexes) {
|
||||
const prevEra = indexes.activeEra.subn(1);
|
||||
const lastPayouts = allInfo
|
||||
.map(({ stakingLedger }, index) => [stashIds[index], stakingLedger?.lastReward?.unwrapOr(new BN(-1)).addn(1)])
|
||||
@ -64,12 +66,13 @@ function getRewards ([thesePayouts, theseRewards]: [[string, EraIndex][], Derive
|
||||
|
||||
export default function useOwnEraRewards (onlyLatest?: boolean): OwnRewards {
|
||||
const { api } = useApi();
|
||||
const mountedRef = useIsMountedRef();
|
||||
const nextPayouts = useNextPayouts(onlyLatest);
|
||||
const available = useCall<[[string, EraIndex][], DeriveStakerReward[][]]>(nextPayouts && api.derive.staking?.stakerRewardsMulti as any, nextPayouts, { withParams: true });
|
||||
const [state, setState] = useState<OwnRewards>({ rewardCount: 0 });
|
||||
|
||||
useEffect((): void => {
|
||||
available && nextPayouts && setState(
|
||||
mountedRef.current && available && nextPayouts && setState(
|
||||
getRewards(available, nextPayouts)
|
||||
);
|
||||
}, [available, nextPayouts]);
|
||||
|
||||
@ -10,6 +10,7 @@ import { Option } from '@polkadot/types';
|
||||
import useAccounts from './useAccounts';
|
||||
import useApi from './useApi';
|
||||
import useCall from './useCall';
|
||||
import useIsMountedRef from './useIsMountedRef';
|
||||
|
||||
type IsInKeyring = boolean;
|
||||
|
||||
@ -33,13 +34,14 @@ function getStashes (allAccounts: string[], ownBonded: Option<AccountId>[], ownL
|
||||
|
||||
export default function useOwnStashes (): [string, IsInKeyring][] | undefined {
|
||||
const { allAccounts } = useAccounts();
|
||||
const mountedRef = useIsMountedRef();
|
||||
const { api } = useApi();
|
||||
const ownBonded = useCall<Option<AccountId>[]>(api.query.staking?.bonded.multi as any, [allAccounts]);
|
||||
const ownLedger = useCall<Option<StakingLedger>[]>(api.query.staking?.ledger.multi as any, [allAccounts]);
|
||||
const [state, setState] = useState<[string, IsInKeyring][] | undefined>();
|
||||
|
||||
useEffect((): void => {
|
||||
allAccounts && ownBonded && ownLedger && setState(
|
||||
mountedRef.current && allAccounts && ownBonded && ownLedger && setState(
|
||||
getStashes(allAccounts, ownBonded, ownLedger)
|
||||
);
|
||||
}, [allAccounts, ownBonded, ownLedger]);
|
||||
@ -48,11 +50,12 @@ export default function useOwnStashes (): [string, IsInKeyring][] | undefined {
|
||||
}
|
||||
|
||||
export function useOwnStashIds (): string[] | undefined {
|
||||
const mountedRef = useIsMountedRef();
|
||||
const ownStashes = useOwnStashes();
|
||||
const [stashIds, setStashIds] = useState<string[] | undefined>();
|
||||
|
||||
useEffect((): void => {
|
||||
ownStashes && setStashIds(
|
||||
mountedRef.current && ownStashes && setStashIds(
|
||||
ownStashes.map(([stashId]) => stashId)
|
||||
);
|
||||
}, [ownStashes]);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user