Add trackStream, impl. in staking overview (#1891)
* Add trackStream, impl. in staking overview * Use trackStream in Address * Fix linting * Add reward displays
This commit is contained in:
parent
d5857436fe
commit
488bfef0c4
@ -10,10 +10,9 @@ import { ValidatorFilter } from '../types';
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { withCalls, withMulti } from '@polkadot/react-api';
|
||||
import { AddressCard, AddressMini, Badge, Expander, Icon } from '@polkadot/react-components';
|
||||
import { classes } from '@polkadot/react-components/util';
|
||||
import { useApiContext } from '@polkadot/react-hooks';
|
||||
import { trackStream, useApiContext } from '@polkadot/react-hooks';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import translate from '../translate';
|
||||
@ -24,13 +23,13 @@ interface Props extends I18nProps {
|
||||
className?: string;
|
||||
defaultName: string;
|
||||
filter: ValidatorFilter;
|
||||
hasQueries: boolean;
|
||||
isElected: boolean;
|
||||
isFavorite: boolean;
|
||||
lastAuthors?: string[];
|
||||
myAccounts: string[];
|
||||
points?: Points;
|
||||
recentlyOnline?: DerivedHeartbeats;
|
||||
stakingInfo?: DerivedStaking;
|
||||
toggleFavorite: (accountId: string) => void;
|
||||
withNominations?: boolean;
|
||||
}
|
||||
@ -47,8 +46,10 @@ interface StakingState {
|
||||
|
||||
const WITH_VALIDATOR_PREFS = { validatorPayment: true };
|
||||
|
||||
function Address ({ address, authorsMap, className, defaultName, filter, isElected, isFavorite, lastAuthors, myAccounts, points, recentlyOnline, stakingInfo, t, toggleFavorite, withNominations = true }: Props): React.ReactElement<Props> | null {
|
||||
function Address ({ address, authorsMap, className, defaultName, filter, hasQueries, isElected, isFavorite, lastAuthors, myAccounts, points, recentlyOnline, t, toggleFavorite, withNominations = true }: Props): React.ReactElement<Props> | null {
|
||||
const { api, isSubstrateV2 } = useApiContext();
|
||||
// FIXME Any horrors, caused by trackStream
|
||||
const stakingInfo = trackStream<DerivedStaking>(api.derive.staking.info as any, [address]);
|
||||
const [extraInfo, setExtraInfo] = useState<[React.ReactNode, React.ReactNode][] | undefined>();
|
||||
const [hasActivity, setHasActivity] = useState(true);
|
||||
const [{ balanceOpts, controllerId, hasNominators, isNominatorMe, nominators, sessionId, stashId }, setStakingState] = useState<StakingState>({
|
||||
@ -188,7 +189,7 @@ function Address ({ address, authorsMap, className, defaultName, filter, isElect
|
||||
}
|
||||
isDisabled={isSubstrateV2 && !hasActivity}
|
||||
overlay={
|
||||
api.query.imOnline?.authoredBlocks && (
|
||||
hasQueries && api.query.imOnline?.authoredBlocks && (
|
||||
<Icon
|
||||
className='staking--stats'
|
||||
name='line graph'
|
||||
@ -223,7 +224,7 @@ function Address ({ address, authorsMap, className, defaultName, filter, isElect
|
||||
);
|
||||
}
|
||||
|
||||
export default withMulti(
|
||||
export default translate(
|
||||
styled(Address)`
|
||||
.extras {
|
||||
display: inline-block;
|
||||
@ -295,12 +296,5 @@ export default withMulti(
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
}
|
||||
`,
|
||||
translate,
|
||||
withCalls<Props>(
|
||||
['derive.staking.info', {
|
||||
paramName: 'address',
|
||||
propName: 'stakingInfo'
|
||||
}]
|
||||
)
|
||||
`
|
||||
);
|
||||
|
||||
@ -18,6 +18,7 @@ import Address from './Address';
|
||||
|
||||
interface Props extends I18nProps {
|
||||
authorsMap: Record<string, string>;
|
||||
hasQueries: boolean;
|
||||
lastAuthors?: string[];
|
||||
next: string[];
|
||||
recentlyOnline?: DerivedHeartbeats;
|
||||
@ -55,7 +56,7 @@ function accountsToString (accounts: AccountId[]): string[] {
|
||||
return accounts.map((accountId): string => accountId.toString());
|
||||
}
|
||||
|
||||
function CurrentList ({ authorsMap, lastAuthors, next, recentlyOnline, stakingOverview, t }: Props): React.ReactElement<Props> {
|
||||
function CurrentList ({ authorsMap, hasQueries, lastAuthors, next, recentlyOnline, stakingOverview, t }: Props): React.ReactElement<Props> {
|
||||
const { isSubstrateV2 } = useApiContext();
|
||||
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE);
|
||||
const [filter, setFilter] = useState<ValidatorFilter>('all');
|
||||
@ -84,6 +85,7 @@ function CurrentList ({ authorsMap, lastAuthors, next, recentlyOnline, stakingOv
|
||||
authorsMap={authorsMap}
|
||||
defaultName={defaultName}
|
||||
filter={filter}
|
||||
hasQueries={hasQueries}
|
||||
isElected={isElected}
|
||||
isFavorite={isFavorite}
|
||||
lastAuthors={lastAuthors}
|
||||
|
||||
@ -14,7 +14,7 @@ import Summary from './Summary';
|
||||
|
||||
interface Props extends BareProps, ComponentProps {}
|
||||
|
||||
export default function Overview ({ allControllers, allStashes, className, recentlyOnline, stakingOverview }: Props): React.ReactElement<Props> {
|
||||
export default function Overview ({ allControllers, hasQueries, allStashes, className, recentlyOnline, stakingOverview }: Props): React.ReactElement<Props> {
|
||||
const { isSubstrateV2 } = useApiContext();
|
||||
const { byAuthor, lastBlockAuthors, lastBlockNumber } = useContext(BlockAuthorsContext);
|
||||
const [next, setNext] = useState<string[]>([]);
|
||||
@ -41,6 +41,7 @@ export default function Overview ({ allControllers, allStashes, className, recen
|
||||
/>
|
||||
<CurrentList
|
||||
authorsMap={byAuthor}
|
||||
hasQueries={hasQueries}
|
||||
lastAuthors={lastBlockAuthors}
|
||||
next={next}
|
||||
recentlyOnline={recentlyOnline}
|
||||
|
||||
@ -3,31 +3,29 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { I18nProps } from '@polkadot/react-components/types';
|
||||
import { Balance, BlockNumber, Hash, Exposure, SessionIndex } from '@polkadot/types/interfaces';
|
||||
import { Balance, Hash, Exposure, SessionIndex } from '@polkadot/types/interfaces';
|
||||
import { SessionRewards, Slash } from '@polkadot/react-hooks/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 { withCalls } from '@polkadot/react-api';
|
||||
import { getHistoric } from '@polkadot/react-api/util';
|
||||
import { useApiContext } from '@polkadot/react-hooks';
|
||||
import { trackStream, useApiContext } from '@polkadot/react-hooks';
|
||||
import { u32 } from '@polkadot/types';
|
||||
import { formatBalance, formatNumber } from '@polkadot/util';
|
||||
|
||||
import { MAX_SESSIONS } from '../constants';
|
||||
import translate from '../translate';
|
||||
|
||||
interface Props extends I18nProps {
|
||||
blockCounts?: BN[];
|
||||
className?: string;
|
||||
currentIndex: SessionIndex;
|
||||
sessionRewards: SessionRewards[];
|
||||
startNumber: BlockNumber;
|
||||
validatorId: string;
|
||||
}
|
||||
|
||||
type LineData = (BN | number)[][];
|
||||
type LineDataEntry = (BN | number)[];
|
||||
|
||||
type LineData = LineDataEntry[];
|
||||
|
||||
interface SplitEntry {
|
||||
colors: string[];
|
||||
@ -39,37 +37,22 @@ type SplitData = SplitEntry[];
|
||||
|
||||
const COLORS_MINE = ['#ff8c00'];
|
||||
const COLORS_OTHER = ['#acacac'];
|
||||
const COLORS_REWARD = ['#8c2200', '#008c22', '#acacac'];
|
||||
const COLORS_BLOCKS = [undefined, '#acacac'];
|
||||
|
||||
function getIndexRange (currentIndex: SessionIndex): BN[] {
|
||||
const range: BN[] = [];
|
||||
let thisIndex: BN = currentIndex;
|
||||
|
||||
while (thisIndex.gtn(0) && range.length < MAX_SESSIONS) {
|
||||
range.push(thisIndex);
|
||||
|
||||
thisIndex = thisIndex.subn(1);
|
||||
}
|
||||
|
||||
return range.reverse();
|
||||
}
|
||||
|
||||
function extractStake (values: [BN, Hash, Exposure][], divisor: BN): [string[], LineData] {
|
||||
function extractStake (values: [Hash, Exposure][], divisor: BN): LineData {
|
||||
return [
|
||||
values.map(([bn]): string => formatNumber(bn)),
|
||||
[
|
||||
values.map(([,, { total }]): BN =>
|
||||
total.unwrap().div(divisor))
|
||||
// exposures.map(({ own }): BN =>
|
||||
// own.unwrap().div(divisor)),
|
||||
// exposures.map(({ others }): BN =>
|
||||
// others.reduce((total, { value }): BN => total.add(value.unwrap()), new BN(0)).div(divisor))
|
||||
]
|
||||
values.map(([, { total }]): BN =>
|
||||
total.unwrap().div(divisor))
|
||||
// exposures.map(({ own }): BN =>
|
||||
// own.unwrap().div(divisor)),
|
||||
// exposures.map(({ others }): BN =>
|
||||
// others.reduce((total, { value }): BN => total.add(value.unwrap()), new BN(0)).div(divisor))
|
||||
];
|
||||
}
|
||||
|
||||
function extractSplit (values: [BN, Hash, Exposure][], validatorId: string): SplitData | null {
|
||||
const last = values[values.length - 1][2];
|
||||
function extractSplit (values: [Hash, Exposure][], validatorId: string): SplitData | null {
|
||||
const last = values[values.length - 1][1];
|
||||
const total = last.total.unwrap();
|
||||
|
||||
if (total.eqn(0)) {
|
||||
@ -96,8 +79,17 @@ function extractEraSlash (validatorId: string, slashes: Slash[]): BN {
|
||||
}, new BN(0));
|
||||
}
|
||||
|
||||
function Validator ({ blockCounts, className, currentIndex, sessionRewards, startNumber, t, validatorId }: Props): React.ReactElement<Props> {
|
||||
function balanceToNumber (amount: BN, divisor: BN): number {
|
||||
return amount.muln(1000).div(divisor).toNumber() / 1000;
|
||||
}
|
||||
|
||||
function Validator ({ className, sessionRewards, t, validatorId }: Props): React.ReactElement<Props> {
|
||||
const { api } = useApiContext();
|
||||
// FIXME There is something seriously wrong in these two with "any" horrors
|
||||
const blockCounts = trackStream<u32[]>(api.query.imOnline?.authoredBlocks?.multi as any, [sessionRewards, validatorId], {
|
||||
paramMap: ([sessionRewards, validatorId]: [SessionRewards[], string]): any =>
|
||||
[sessionRewards.map(({ sessionIndex }): [SessionIndex, string] => [sessionIndex, validatorId])]
|
||||
});
|
||||
const [blocksLabels, setBlocksLabels] = useState<string[]>([]);
|
||||
const [blocksChart, setBlocksChart] = useState<LineData | null>(null);
|
||||
const [{ rewardsChart, rewardsLabels }, setRewardsInfo] = useState<{ rewardsChart: LineData | null; rewardsLabels: string[] }>({ rewardsChart: null, rewardsLabels: [] });
|
||||
@ -106,53 +98,65 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star
|
||||
const divisor = new BN('1'.padEnd(formatBalance.getDefaults().decimals + 1, '0'));
|
||||
|
||||
useEffect((): void => {
|
||||
api.isReady.then(async (): Promise<void> => {
|
||||
const values = await getHistoric<Exposure>(api, 'staking.stakers', [validatorId], {
|
||||
interval: (api.consts.babe?.epochDuration as BlockNumber || new BN(500)).muln(2).divn(3),
|
||||
max: MAX_SESSIONS,
|
||||
startNumber
|
||||
});
|
||||
const [stakeLabels, stakeChart] = extractStake(values, divisor);
|
||||
const splitChart = extractSplit(values, validatorId);
|
||||
const splitMax = splitChart ? Math.min(Math.ceil(splitChart[0].value), 100) : 100;
|
||||
if (!splitChart) {
|
||||
const hashes = sessionRewards.map(({ blockHash }): Hash => blockHash);
|
||||
const stakeLabels = sessionRewards.map(({ sessionIndex }): string => formatNumber(sessionIndex));
|
||||
|
||||
setStakeInfo({ stakeChart, stakeLabels });
|
||||
setSplitInfo({ splitChart, splitMax });
|
||||
});
|
||||
}, []);
|
||||
api.isReady.then(async (): Promise<void> => {
|
||||
const values = await getHistoric<Exposure>(api, 'staking.stakers', [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 => {
|
||||
const rewardsLabels: string[] = [];
|
||||
const rewardsChart: LineData = [[]];
|
||||
if (blockCounts) {
|
||||
const rewardsLabels: string[] = [];
|
||||
const rewardsChart: LineData = [[], [], []];
|
||||
let total = new BN(0);
|
||||
|
||||
sessionRewards.forEach(({ sessionIndex, slashes }): void => {
|
||||
// this shows the start of the new era, however rewards are for previous
|
||||
rewardsLabels.push(formatNumber(sessionIndex.subn(1)));
|
||||
sessionRewards.forEach(({ blockNumber, reward, sessionIndex, slashes }, index): void => {
|
||||
// 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(
|
||||
extractEraSlash(validatorId, slashes).muln(1000).div(divisor).toNumber() / 1000
|
||||
);
|
||||
});
|
||||
const neg = extractEraSlash(validatorId, slashes);
|
||||
const pos = index
|
||||
? reward.mul(blockCounts[index - 1]).div(blockNumber.sub(sessionRewards[index - 1].blockNumber))
|
||||
: new BN(0);
|
||||
|
||||
setRewardsInfo({ rewardsChart, rewardsLabels });
|
||||
}, [sessionRewards, validatorId]);
|
||||
// add this to the total
|
||||
total = total.add(neg).add(pos);
|
||||
|
||||
// calculate and format to 3 decimals
|
||||
rewardsChart[0].push(balanceToNumber(neg, divisor));
|
||||
rewardsChart[1].push(balanceToNumber(pos, divisor));
|
||||
rewardsChart[2].push(balanceToNumber(total.divn(index), divisor));
|
||||
});
|
||||
|
||||
setRewardsInfo({ rewardsChart, rewardsLabels });
|
||||
}
|
||||
}, [blockCounts, sessionRewards, validatorId]);
|
||||
|
||||
useEffect((): void => {
|
||||
setBlocksLabels(
|
||||
getIndexRange(currentIndex).map((index): string => formatNumber(index))
|
||||
sessionRewards.map(({ sessionIndex }): string => formatNumber(sessionIndex))
|
||||
);
|
||||
}, [currentIndex]);
|
||||
}, [sessionRewards]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (blockCounts) {
|
||||
const avgSet: number[] = [];
|
||||
const idxSet: BN[] = [];
|
||||
|
||||
blockCounts.reduce((total: BN, value, index): BN => {
|
||||
blockCounts.reduce((total: BN, value: u32, index: number): BN => {
|
||||
total = total.add(value);
|
||||
|
||||
avgSet.push(total.toNumber() / (index + 1));
|
||||
avgSet.push(total.muln(100).divn(index + 1).toNumber() / 100);
|
||||
idxSet.push(value);
|
||||
|
||||
return total;
|
||||
@ -160,7 +164,7 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star
|
||||
|
||||
setBlocksChart([idxSet, avgSet]);
|
||||
}
|
||||
}, [blockCounts, blocksLabels]);
|
||||
}, [blockCounts]);
|
||||
|
||||
return (
|
||||
<Columar className={className}>
|
||||
@ -169,7 +173,7 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star
|
||||
<>
|
||||
{blocksChart && (
|
||||
<div className='staking--Chart'>
|
||||
<h1>{t('blocks per session')}</h1>
|
||||
<h1>{t('blocks produced')}</h1>
|
||||
<Chart.Line
|
||||
colors={COLORS_BLOCKS}
|
||||
labels={blocksLabels}
|
||||
@ -180,11 +184,11 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star
|
||||
)}
|
||||
{rewardsChart && (
|
||||
<div className='staking--Chart'>
|
||||
<h1>{t('slashed per session')}</h1>
|
||||
<h1>{t('rewards & slashes')}</h1>
|
||||
<Chart.Line
|
||||
colors={COLORS_BLOCKS}
|
||||
colors={COLORS_REWARD}
|
||||
labels={rewardsLabels}
|
||||
legends={[t('slashed'), t('rewarded')]}
|
||||
legends={[t('slashed'), t('rewards (est.)'), t('average')]}
|
||||
values={rewardsChart}
|
||||
/>
|
||||
</div>
|
||||
@ -222,13 +226,4 @@ function Validator ({ blockCounts, className, currentIndex, sessionRewards, star
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(
|
||||
withCalls<Props>(
|
||||
['query.imOnline.authoredBlocks', {
|
||||
isMulti: true,
|
||||
propName: 'blockCounts',
|
||||
paramPick: ({ currentIndex, validatorId }: Props): [BN, string][] =>
|
||||
getIndexRange(currentIndex).map((index): [BN, string] => [index, validatorId])
|
||||
}]
|
||||
)(Validator)
|
||||
);
|
||||
export default translate(Validator);
|
||||
|
||||
@ -3,10 +3,9 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { I18nProps } from '@polkadot/react-components/types';
|
||||
import { BlockNumber } from '@polkadot/types/interfaces';
|
||||
import { ComponentProps } from '../types';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { Button, InputAddressSimple } from '@polkadot/react-components';
|
||||
|
||||
@ -47,16 +46,9 @@ interface Props extends I18nProps, ComponentProps, RouteComponentProps<{}> {
|
||||
// />
|
||||
// </Input>
|
||||
|
||||
function Query ({ bestNumber, className, sessionRewards, stakingOverview, match: { params: { value } }, t }: Props): React.ReactElement<Props> {
|
||||
const [startNumber, setStartNumber] = useState<BlockNumber | undefined>();
|
||||
function Query ({ className, sessionRewards, match: { params: { value } }, t }: Props): React.ReactElement<Props> {
|
||||
const [validatorId, setValidatorId] = useState<string | null>(value || null);
|
||||
|
||||
useEffect((): void => {
|
||||
if (bestNumber && !startNumber) {
|
||||
setStartNumber(bestNumber);
|
||||
}
|
||||
}, [bestNumber, startNumber]);
|
||||
|
||||
const _onQuery = (): void => {
|
||||
if (validatorId) {
|
||||
window.location.hash = `/staking/query/${validatorId}`;
|
||||
@ -79,11 +71,9 @@ function Query ({ bestNumber, className, sessionRewards, stakingOverview, match:
|
||||
onClick={_onQuery}
|
||||
/>
|
||||
</InputAddressSimple>
|
||||
{value && startNumber && stakingOverview && (
|
||||
{value && (
|
||||
<Validator
|
||||
currentIndex={stakingOverview.currentIndex}
|
||||
sessionRewards={sessionRewards}
|
||||
startNumber={startNumber}
|
||||
validatorId={value}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -16,8 +16,8 @@ import styled from 'styled-components';
|
||||
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';
|
||||
import { useApiContext, useSessionRewards } from '@polkadot/react-hooks';
|
||||
import { withMulti, withObservable } from '@polkadot/react-api';
|
||||
import { trackStream, useApiContext, useSessionRewards } from '@polkadot/react-hooks';
|
||||
import accountObservable from '@polkadot/ui-keyring/observable/accounts';
|
||||
|
||||
import Accounts from './Actions/Accounts';
|
||||
@ -29,19 +29,32 @@ import translate from './translate';
|
||||
|
||||
interface Props extends AppProps, ApiProps, I18nProps {
|
||||
allAccounts?: SubjectInfo;
|
||||
allStashesAndControllers?: [string[], string[]];
|
||||
bestNumber?: BlockNumber;
|
||||
recentlyOnline?: DerivedHeartbeats;
|
||||
stakingOverview?: DerivedStakingOverview;
|
||||
}
|
||||
|
||||
const EMPY_ACCOUNTS: string[] = [];
|
||||
const EMPTY_ALL: [string[], string[]] = [EMPY_ACCOUNTS, EMPY_ACCOUNTS];
|
||||
|
||||
function App ({ allAccounts, allStashesAndControllers: [allStashes, allControllers] = EMPTY_ALL, basePath, bestNumber, className, recentlyOnline, stakingOverview, t }: Props): React.ReactElement<Props> {
|
||||
function transformStakingControllers ([stashes, controllers]: [AccountId[], Option<AccountId>[]]): [string[], string[]] {
|
||||
return [
|
||||
stashes.map((accountId): string => accountId.toString()),
|
||||
controllers
|
||||
.filter((optId): boolean => optId.isSome)
|
||||
.map((accountId): string => accountId.unwrap().toString())
|
||||
];
|
||||
}
|
||||
|
||||
function App ({ allAccounts, basePath, className, t }: Props): React.ReactElement<Props> {
|
||||
const { api } = useApiContext();
|
||||
const stakingControllers = trackStream<[string[], string[]]>(api.derive.staking.controllers, [], { transform: transformStakingControllers });
|
||||
const bestNumber = trackStream<BlockNumber>(api.derive.chain.bestNumber, []);
|
||||
const recentlyOnline = trackStream<DerivedHeartbeats>(api.derive.imOnline.receivedHeartbeats, []);
|
||||
const stakingOverview = trackStream<DerivedStakingOverview>(api.derive.staking.overview, []);
|
||||
const sessionRewards = useSessionRewards(MAX_SESSIONS);
|
||||
const routeMatch = useRouteMatch({ path: basePath, strict: true });
|
||||
|
||||
const hasAccounts = !!allAccounts && Object.keys(allAccounts).length !== 0;
|
||||
const hasQueries = hasAccounts && !!(api.query.imOnline?.authoredBlocks);
|
||||
const [allStashes, allControllers] = stakingControllers || EMPTY_ALL;
|
||||
const _renderComponent = (Component: React.ComponentType<ComponentProps>, className?: string): () => React.ReactNode => {
|
||||
// eslint-disable-next-line react/display-name
|
||||
return (): React.ReactNode => {
|
||||
@ -56,6 +69,8 @@ function App ({ allAccounts, allStashesAndControllers: [allStashes, allControlle
|
||||
allStashes={allStashes}
|
||||
bestNumber={bestNumber}
|
||||
className={className}
|
||||
hasAccounts={hasAccounts}
|
||||
hasQueries={hasQueries}
|
||||
recentlyOnline={recentlyOnline}
|
||||
sessionRewards={sessionRewards}
|
||||
stakingOverview={stakingOverview}
|
||||
@ -71,11 +86,11 @@ function App ({ allAccounts, allStashesAndControllers: [allStashes, allControlle
|
||||
<Tabs
|
||||
basePath={basePath}
|
||||
hidden={
|
||||
!allAccounts || Object.keys(allAccounts).length === 0
|
||||
? ['actions', 'query']
|
||||
: api.query.imOnline?.authoredBlocks
|
||||
hasAccounts
|
||||
? hasQueries
|
||||
? []
|
||||
: ['query']
|
||||
: ['actions', 'query']
|
||||
}
|
||||
items={[
|
||||
{
|
||||
@ -124,19 +139,5 @@ export default withMulti(
|
||||
}
|
||||
`,
|
||||
translate,
|
||||
withCalls<Props>(
|
||||
['derive.chain.bestNumber', { propName: 'bestNumber' }],
|
||||
['derive.imOnline.receivedHeartbeats', { propName: 'recentlyOnline' }],
|
||||
['derive.staking.controllers', {
|
||||
propName: 'allStashesAndControllers',
|
||||
transform: ([stashes, controllers]: [AccountId[], Option<AccountId>[]]): [string[], string[]] => [
|
||||
stashes.map((accountId): string => accountId.toString()),
|
||||
controllers
|
||||
.filter((optId): boolean => optId.isSome)
|
||||
.map((accountId): string => accountId.unwrap().toString())
|
||||
]
|
||||
}],
|
||||
['derive.staking.overview', { propName: 'stakingOverview' }]
|
||||
),
|
||||
withObservable(accountObservable.subject, { propName: 'allAccounts' })
|
||||
);
|
||||
|
||||
@ -15,6 +15,8 @@ export interface ComponentProps {
|
||||
allStashes: string[];
|
||||
bestNumber?: BlockNumber;
|
||||
className?: string;
|
||||
hasAccounts: boolean;
|
||||
hasQueries: boolean;
|
||||
recentlyOnline?: DerivedHeartbeats;
|
||||
sessionRewards: SessionRewards[];
|
||||
stakingOverview?: DerivedStakingOverview;
|
||||
|
||||
@ -5,21 +5,16 @@
|
||||
import { Hash } from '@polkadot/types/interfaces';
|
||||
import { Codec } from '@polkadot/types/types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import ApiPromise from '@polkadot/api/promise';
|
||||
|
||||
import getHashes, { HashesOptions } from './hashes';
|
||||
|
||||
export default async function getHistory <T extends Codec> (api: ApiPromise, endpoint: string, params: any[], options: HashesOptions): Promise<[BN, Hash, T][]> {
|
||||
export default async function getHistory <T extends Codec> (api: ApiPromise, endpoint: string, params: any[], hashes: Hash[]): Promise<[Hash, T][]> {
|
||||
const [mod, fn] = endpoint.split('.');
|
||||
const numbers = await getHashes(api, options);
|
||||
const results = await Promise.all(numbers.map(([, hash]): Promise<T> =>
|
||||
const results = await Promise.all(hashes.map((hash): Promise<T> =>
|
||||
api.query[mod][fn].at(hash, ...params) as Promise<T>
|
||||
));
|
||||
|
||||
return results.map((value, index): [BN, Hash, T] => [
|
||||
numbers[index][0],
|
||||
numbers[index][1],
|
||||
return results.map((value, index): [Hash, T] => [
|
||||
hashes[index],
|
||||
value
|
||||
]);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
export { default as trackStream } from './trackStream';
|
||||
export { default as useApiContext } from './useApiContext';
|
||||
export { default as useDebounce } from './useDebounce';
|
||||
export { default as useFavorites } from './useFavorites';
|
||||
|
||||
90
packages/react-hooks/src/trackStream.tsx
Normal file
90
packages/react-hooks/src/trackStream.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
// Copyright 2017-2019 @polkadot/react-hooks 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 { useEffect, useRef, useState } from 'react';
|
||||
import { isNull, isUndefined } from '@polkadot/util';
|
||||
|
||||
type Params = [] | [any] | [any, any] | [any, any, any];
|
||||
|
||||
interface Options <T> {
|
||||
paramMap?: (params: any) => any;
|
||||
transform?: (value: any) => T;
|
||||
}
|
||||
|
||||
interface TrackFnCallback <T> {
|
||||
(value: T): void;
|
||||
}
|
||||
|
||||
type Unsub = () => void;
|
||||
|
||||
interface TrackFn <T> {
|
||||
(a: any, b: any, c: any, cb: TrackFnCallback<T>): Promise<Unsub>;
|
||||
(a: any, b: any, cb: TrackFnCallback<T>): Promise<Unsub>;
|
||||
(a: any, cb: TrackFnCallback<T>): Promise<Unsub>;
|
||||
(cb: TrackFnCallback<T>): Promise<Unsub>;
|
||||
}
|
||||
|
||||
const dummySubscribe = Promise.resolve((): void => {
|
||||
// we do nothing, this is a noop
|
||||
});
|
||||
|
||||
function transformIdentity (value: any): any {
|
||||
return value;
|
||||
}
|
||||
|
||||
// extract the serialized and mapped params, all ready for use in our call
|
||||
function extractParams (params: any[], paramMap: (params: any[]) => any): [string, Params | null] {
|
||||
return [
|
||||
JSON.stringify({ a: params }),
|
||||
params.length === 0 || !params.some((param): boolean => isNull(param) || isUndefined(null))
|
||||
? paramMap(params)
|
||||
: null
|
||||
];
|
||||
}
|
||||
|
||||
// tracks a stream, typically an api.* call that
|
||||
// - returns a promise with an unsubscription
|
||||
// - has a callback to set the value
|
||||
// FIXME The typings here need some serious TLC
|
||||
export default function trackStream <T> (fn: TrackFn<T> | undefined, params: any, { paramMap = transformIdentity, transform = transformIdentity }: Options<T> = {}): T | undefined {
|
||||
const [value, setValue] = useState<T | undefined>();
|
||||
const tracker = useRef<{ serialized: string | null; subscriber: Promise<Unsub> }>({ serialized: null, subscriber: dummySubscribe });
|
||||
|
||||
const _subscribe = (params: Params): void => {
|
||||
tracker.current.subscriber = fn
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore We tried to get the typings right, close but no cigar...
|
||||
? fn(...params, (value: any): T => setValue(transform(value)))
|
||||
: dummySubscribe;
|
||||
};
|
||||
const _unsubscribe = (): void => {
|
||||
tracker.current.subscriber.then((fn): void => fn());
|
||||
};
|
||||
|
||||
// initial round, subscribe once
|
||||
useEffect((): () => void => {
|
||||
const [serialized, mappedParams] = extractParams(params, paramMap);
|
||||
|
||||
tracker.current.serialized = serialized;
|
||||
|
||||
if (mappedParams) {
|
||||
_subscribe(mappedParams);
|
||||
}
|
||||
|
||||
return _unsubscribe;
|
||||
}, []);
|
||||
|
||||
// on changes, re-subscribe
|
||||
useEffect((): void => {
|
||||
const [serialized, mappedParams] = extractParams(params, paramMap);
|
||||
|
||||
if (mappedParams && serialized !== tracker.current.serialized) {
|
||||
tracker.current.serialized = serialized;
|
||||
_unsubscribe();
|
||||
_subscribe(mappedParams);
|
||||
}
|
||||
}, [params]);
|
||||
|
||||
return value;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user