UI support for simple payouts (#2509)

* UI support for simple payouts

* payout modal

* Adjust with era filtering

* Slow as molasses, but seems working

* Adjust th vertical alignment

* Bump API

* Update

* Cleanup reward retrievals

* linting

* Bumps

* Some rendering optimizations

* Re-add counter

* Fix new header linting

* Actual API bump

* Bumps

* Adjustments

* Adjust payouts as stand-alone tab

* Adjust nominations

* rm unused style

* change offsets

* Adjust
This commit is contained in:
Jaco Greeff 2020-04-08 16:39:32 +02:00 committed by GitHub
parent bc48e433b9
commit d677d64817
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 1893 additions and 1402 deletions

View File

@ -1 +1,5 @@
// Copyright 2017-2020 @polkadot/apps authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
module.exports = require('@polkadot/dev/config/babel');

View File

@ -1,4 +1,7 @@
/* eslint-disable @typescript-eslint/no-var-requires */
// Copyright 2017-2020 @polkadot/apps authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
const config = require('@polkadot/dev/config/jest');
const findPackages = require('./scripts/findPackages');
@ -18,6 +21,6 @@ module.exports = Object.assign({}, config, {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'empty/object'
},
transformIgnorePatterns: [
'<rootDir>/node_modules/(?!edgeware-node-types/.*)'
'<rootDir>/node_modules'
]
});

View File

@ -8,12 +8,12 @@
"packages/*"
],
"resolutions": {
"@polkadot/api": "^1.10.0-beta.7",
"@polkadot/api-contract": "^1.10.0-beta.7",
"@polkadot/keyring": "^2.7.1",
"@polkadot/types": "^1.10.0-beta.7",
"@polkadot/util": "^2.7.1",
"@polkadot/util-crypto": "^2.7.1",
"@polkadot/api": "^1.10.0-beta.17",
"@polkadot/api-contract": "^1.10.0-beta.17",
"@polkadot/keyring": "^2.8.0-beta.7",
"@polkadot/types": "^1.10.0-beta.17",
"@polkadot/util": "^2.8.0-beta.7",
"@polkadot/util-crypto": "^2.8.0-beta.7",
"babel-core": "^7.0.0-bridge.0",
"typescript": "^3.8.3"
},
@ -37,13 +37,13 @@
"@babel/core": "^7.9.0",
"@babel/register": "^7.9.0",
"@babel/runtime": "^7.9.2",
"@polkadot/dev": "^0.52.4",
"@polkadot/ts": "^0.3.15",
"@polkadot/dev": "^0.52.8",
"@polkadot/ts": "^0.3.18",
"@types/bn.js": "^4.11.6",
"@types/chart.js": "^2.9.18",
"@types/file-saver": "^2.0.1",
"@types/i18next": "^13.0.0",
"@types/jest": "^25.1.5",
"@types/jest": "^25.2.1",
"@types/react-beautiful-dnd": "^12.1.2",
"@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-dom": "^16.9.6",

View File

@ -29,5 +29,6 @@ export default {
Address: 'AccountId',
LookupSource: 'AccountId',
// previous substrate versions
ReferendumInfo: 'ReferendumInfoTo239'
ReferendumInfo: 'ReferendumInfoTo239',
StakingLedger: 'StakingLedgerTo240'
};

View File

@ -2,12 +2,14 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import * as definitions from 'edgeware-node-types/dist/definitions';
import signaling from 'edgeware-node-types/dist/signaling/definitions';
import treasuryRewards from 'edgeware-node-types/dist/treasuryRewards/definitions';
import voting from 'edgeware-node-types/dist//voting/definitions';
import { typesFromDefs } from '../util';
export default {
...typesFromDefs(definitions),
...typesFromDefs({ signaling, treasuryRewards, voting }),
Address: 'GenericAddress',
Keys: 'SessionKeys4',
// previous substrate versions

View File

@ -10,5 +10,6 @@
export default {
Address: 'AccountId',
Keys: 'SessionKeys5',
LookupSource: 'AccountId'
LookupSource: 'AccountId',
StakingLedger: 'StakingLedgerTo240'
};

View File

@ -15,7 +15,7 @@
"dependencies": {
"@babel/polyfill": "^7.8.7",
"@babel/runtime": "^7.9.2",
"@polkadot/dev": "^0.52.4",
"@polkadot/dev": "^0.52.8",
"@polkadot/react-components": "0.40.0-beta.251",
"@polkadot/react-signer": "0.40.0-beta.251",
"query-string": "^6.11.1"

View File

@ -13,8 +13,8 @@
"dependencies": {
"@babel/runtime": "^7.9.2",
"@polkadot/react-components": "0.40.0-beta.251",
"@polkadot/react-qr": "^0.52.0-beta.24",
"@polkadot/vanitygen": "^0.11.0-beta.14",
"@polkadot/react-qr": "^0.52.0-beta.30",
"@polkadot/vanitygen": "^0.11.0-beta.17",
"detect-browser": "^5.0.0",
"file-saver": "^2.0.2"
}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/camelcase */
// Copyright 2017-2020 @polkadot/app-123code authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/camelcase */
// Copyright 2017-2020 @polkadot/app-123code authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

View File

@ -12,6 +12,6 @@
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.9.2",
"@polkadot/api-contract": "^1.10.0-beta.7"
"@polkadot/api-contract": "^1.10.0-beta.17"
}
}

View File

@ -1,8 +1,9 @@
/* eslint-disable @typescript-eslint/camelcase */
// Copyright 2017-2020 @polkadot/react-components authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
/* eslint-disable @typescript-eslint/camelcase */
import { I18nProps } from '@polkadot/react-components/types';
import { CodeStored } from '@polkadot/app-contracts/types';

View File

@ -1,8 +1,9 @@
/* eslint-disable @typescript-eslint/camelcase */
// Copyright 2017-2020 @polkadot/app-contracts authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
/* eslint-disable @typescript-eslint/camelcase */
import { PrefabWasmModule } from '@polkadot/types/interfaces';
import { I18nProps } from '@polkadot/react-components/types';
import { ApiProps } from '@polkadot/react-api/types';

View File

@ -24,6 +24,7 @@ function Vote ({ electionsInfo }: Props): React.ReactElement<Props> {
const [isVisible, toggleVisible] = useToggle();
const [accountId, setAccountId] = useState<string | null>(null);
const [available, setAvailable] = useState<string[]>([]);
const [defaultVotes, setDefaultVotes] = useState<string[]>([]);
const [votes, setVotes] = useState<string[]>([]);
const [voteValue, setVoteValue] = useState(new BN(0));
@ -42,7 +43,7 @@ function Vote ({ electionsInfo }: Props): React.ReactElement<Props> {
useEffect((): void => {
accountId && api.derive.council.votesOf(accountId).then(({ votes }): void => {
setVotes(
setDefaultVotes(
votes
.map((accountId): string => accountId.toString())
.filter((accountId): boolean => available.includes(accountId))
@ -74,10 +75,10 @@ function Vote ({ electionsInfo }: Props): React.ReactElement<Props> {
<InputAddressMulti
available={available}
availableLabel={t('council candidates')}
defaultValue={defaultVotes}
help={t('Select and order council candidates you wish to vote for.')}
maxCount={MAX_VOTES}
onChange={setVotes}
value={votes}
valueLabel={t('my ordered votes')}
/>
</Modal.Content>

View File

@ -23,7 +23,7 @@ interface Props {
value: DeriveReferendumExt;
}
function Referendum ({ className, value: { allAye, allNay, image, imageHash, index, isPassing, status, voteCountAye, voteCountNay, votedAye, votedNay } }: Props): React.ReactElement<Props> | null {
function Referendum ({ className, value: { allAye, allNay, changeAye, changeNay, image, imageHash, index, isPassing, status, voteCountAye, voteCountNay, votedAye, votedNay } }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const bestNumber = useCall<BlockNumber>(api.derive.chain.bestNumber, []);
@ -55,12 +55,16 @@ function Referendum ({ className, value: { allAye, allNay, image, imageHash, ind
#{formatNumber(enactBlock)}
</td>
<ReferendumVotes
change={changeAye}
count={voteCountAye}
isWinning={isPassing}
total={votedAye}
votes={allAye}
/>
<ReferendumVotes
change={changeNay}
count={voteCountNay}
isWinning={!isPassing}
total={votedNay}
votes={allNay}
/>

View File

@ -6,21 +6,23 @@ import { DeriveReferendumVote } from '@polkadot/api-derive/types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import { Expander } from '@polkadot/react-components';
import { Expander, Icon } from '@polkadot/react-components';
import { FormatBalance } from '@polkadot/react-query';
import { formatNumber } from '@polkadot/util';
import ReferendumVote from './ReferendumVote';
interface Props {
change: BN;
count: number;
isWinning: boolean;
total: BN;
votes: DeriveReferendumVote[];
}
const LOCKS = [1, 10, 20, 30, 40, 50, 60];
function ReferendumVotes ({ count, total, votes }: Props): React.ReactElement<Props> {
function ReferendumVotes ({ change, count, isWinning, total, votes }: Props): React.ReactElement<Props> {
const [sorted, setSorted] = useState<DeriveReferendumVote[]>([]);
useEffect((): void => {
@ -36,7 +38,10 @@ function ReferendumVotes ({ count, total, votes }: Props): React.ReactElement<Pr
return (
<td className='number'>
<Expander summary={<><FormatBalance value={total} />{count ? ` (${formatNumber(count)})` : '' }</>}>
<Expander
summary={<><FormatBalance value={total} />{count ? ` (${formatNumber(count)})` : '' }</>}
summarySub={change.gtn(0) ? <><Icon name={isWinning ? 'arrow alternate circle up outline' : 'arrow alternate circle down outline'} /><FormatBalance value={change} /></> : ''}
>
{sorted.map((vote) =>
<ReferendumVote
key={vote.accountId.toString()}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/camelcase */
// Copyright 2017-2020 @polkadot/react-query authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/camelcase */
// Copyright 2017-2020 @polkadot/app-explorer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/camelcase */
// Copyright 2017-2020 @polkadot/app-explorer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

View File

@ -1,8 +1,9 @@
/* eslint-disable @typescript-eslint/camelcase */
// 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.
/* eslint-disable @typescript-eslint/camelcase */
import { I18nProps } from '@polkadot/react-components/types';
import { ApiProps } from '@polkadot/react-api/types';
import { CalculateBalanceProps } from '../../types';

View File

@ -2,8 +2,6 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DeriveStakingOverview } from '@polkadot/api-derive/types';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { InputAddressMulti, InputAddress, Modal, TxButton } from '@polkadot/react-components';
@ -15,47 +13,41 @@ import { useTranslation } from '../../translate';
interface Props {
className?: string;
controllerId: string;
isOpen: boolean;
next?: string[];
nominees?: string[];
onClose: () => void;
stakingOverview?: DeriveStakingOverview;
stashId: string;
validators: string[];
}
const MAX_NOMINEES = 16;
function Nominate ({ className, controllerId, next, nominees, onClose, stakingOverview, stashId }: Props): React.ReactElement<Props> | null {
function Nominate ({ className, controllerId, isOpen, next, nominees, onClose, stashId, validators }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const [favorites] = useFavorites(STORE_FAVS_BASE);
const [validators, setValidators] = useState<string[]>([]);
const [selection, setSelection] = useState<string[] | undefined>();
const [selection, setSelection] = useState<string[]>([]);
const [available, setAvailable] = useState<string[]>([]);
useEffect((): void => {
!selection && nominees && setSelection(nominees);
}, [selection, nominees]);
useEffect((): void => {
stakingOverview && setValidators(
stakingOverview.validators.map((acc): string => acc.toString())
);
}, [stakingOverview]);
useEffect((): void => {
const shortlist = [
// ensure that the favorite is included in the list of stashes
...favorites.filter((acc): boolean => validators.includes(acc) || (next || []).includes(acc)),
...favorites.filter((acc) => (validators || []).includes(acc) || (next || []).includes(acc)),
// make sure the nominee is not in our favorites already
...(nominees || []).filter((acc): boolean => !favorites.includes(acc))
...(nominees || []).filter((acc) => !favorites.includes(acc))
];
setAvailable([
...shortlist,
...validators.filter((acc): boolean => !shortlist.includes(acc)),
...(next || []).filter((acc): boolean => !shortlist.includes(acc))
...(validators || []).filter((acc) => !shortlist.includes(acc)),
...(next || []).filter((acc) => !shortlist.includes(acc))
]);
}, [favorites, next, nominees, validators]);
if (!isOpen) {
return null;
}
return (
<Modal
className={`staking--Nominating ${className}`}
@ -78,10 +70,10 @@ function Nominate ({ className, controllerId, next, nominees, onClose, stakingOv
available={available}
availableLabel={t('candidate accounts')}
className='medium'
defaultValue={nominees}
help={t('Filter available candidates based on name, address or short account index.')}
maxCount={MAX_NOMINEES}
onChange={setSelection}
value={selection || []}
valueLabel={t('nominated accounts')}
/>
</Modal.Content>

View File

@ -1,8 +1,9 @@
/* eslint-disable @typescript-eslint/camelcase */
// 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.
/* eslint-disable @typescript-eslint/camelcase */
import { AccountId, StakingLedger } from '@polkadot/types/interfaces';
import { I18nProps } from '@polkadot/react-components/types';
import { ApiProps } from '@polkadot/react-api/types';

View File

@ -2,24 +2,19 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { SubmittableExtrinsic } from '@polkadot/api/types';
import { DeriveBalancesAll, DeriveStakingAccount, DeriveStakingOverview, DeriveStakerReward } from '@polkadot/api-derive/types';
import { DeriveBalancesAll, DeriveStakingAccount } from '@polkadot/api-derive/types';
import { AccountId, EraIndex, Exposure, StakingLedger, ValidatorPrefs } from '@polkadot/types/interfaces';
import { Codec, ITuple } from '@polkadot/types/types';
import BN from 'bn.js';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { Trans } from 'react-i18next';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { ApiPromise } from '@polkadot/api';
import { AddressInfo, AddressMini, AddressSmall, Badge, Button, Expander, Menu, Popup, Spinner, StakingBonded, StakingRedeemable, StakingUnbonding, StatusContext, TxButton } from '@polkadot/react-components';
import { AddressInfo, AddressMini, AddressSmall, Button, Expander, Menu, Popup, StakingBonded, StakingRedeemable, StakingUnbonding, TxButton } from '@polkadot/react-components';
import { useAccounts, useApi, useCall, useToggle } from '@polkadot/react-hooks';
import { FormatBalance } from '@polkadot/react-query';
import { u8aConcat, u8aToHex } from '@polkadot/util';
import { useTranslation } from '../../translate';
import useInactives from '../useInactives';
import BondExtra from './BondExtra';
// import ClaimRewards from './ClaimRewards';
import InjectKeys from './InjectKeys';
import Nominate from './Nominate';
import SetControllerAccount from './SetControllerAccount';
@ -27,7 +22,6 @@ import SetRewardDestination from './SetRewardDestination';
import SetSessionKey from './SetSessionKey';
import Unbond from './Unbond';
import Validate from './Validate';
import useInactives from './useInactives';
type ValidatorInfo = ITuple<[ValidatorPrefs, Codec]> | ValidatorPrefs;
@ -39,9 +33,8 @@ interface Props {
isOwnStash: boolean;
next?: string[];
onUpdateType: (stashId: string, type: 'validator' | 'nominator' | 'started' | 'other') => void;
rewards?: DeriveStakerReward[];
stakingOverview?: DeriveStakingOverview;
stashId: string;
validators?: string[];
}
interface StakeState {
@ -97,34 +90,17 @@ function getStakeState (allAccounts: string[], allStashes: string[] | undefined,
};
}
function createPayout (api: ApiPromise, payoutRewards: DeriveStakerReward[]): SubmittableExtrinsic<'promise'> {
return payoutRewards.length === 1
? payoutRewards[0].isValidator
? api.tx.staking.payoutValidator(payoutRewards[0].era)
: api.tx.staking.payoutNominator(payoutRewards[0].era, payoutRewards[0].nominating)
: api.tx.utility.batch(
payoutRewards.map(({ era, isValidator, nominating }): SubmittableExtrinsic<'promise'> =>
isValidator
? api.tx.staking.payoutValidator(era)
: api.tx.staking.payoutNominator(era, nominating)
)
);
}
function Account ({ allStashes, className, isInElection, isOwnStash, next, onUpdateType, rewards, stakingOverview, stashId }: Props): React.ReactElement<Props> {
function Account ({ allStashes, className, isInElection, isOwnStash, next, onUpdateType, stashId, validators }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { queueExtrinsic } = useContext(StatusContext);
const { api } = useApi();
const { allAccounts } = useAccounts();
const validateInfo = useCall<ValidatorInfo>(api.query.staking.validators, [stashId]);
const balancesAll = useCall<DeriveBalancesAll>(api.derive.balances.all as any, [stashId]);
const stakingAccount = useCall<DeriveStakingAccount>(api.derive.staking.account as any, [stashId]);
const [[payoutRewards, payoutEras, payoutTotal], setStakingRewards] = useState<[DeriveStakerReward[], EraIndex[], BN]>([[], [], new BN(0)]);
const [{ controllerId, destination, destinationId, hexSessionIdNext, hexSessionIdQueue, isLoading, isOwnController, isStashNominating, isStashValidating, nominees, sessionIds, validatorPrefs }, setStakeState] = useState<StakeState>({ controllerId: null, destinationId: 0, hexSessionIdNext: null, hexSessionIdQueue: null, isLoading: true, isOwnController: false, isStashNominating: false, isStashValidating: false, sessionIds: [] });
const [activeNoms, setActiveNoms] = useState<string[]>([]);
const inactiveNoms = useInactives(stashId, nominees);
const [isBondExtraOpen, toggleBondExtra] = useToggle();
// const [isPayoutOpen, togglePayout] = useToggle();
const [isInjectOpen, toggleInject] = useToggle();
const [isNominateOpen, toggleNominate] = useToggle();
const [isRewardDestinationOpen, toggleRewardDestination] = useToggle();
@ -139,14 +115,14 @@ function Account ({ allStashes, className, isInElection, isOwnStash, next, onUpd
const state = getStakeState(allAccounts, allStashes, stakingAccount, stashId, validateInfo);
setStakeState(state);
if (state.isStashValidating) {
onUpdateType(stashId, 'validator');
} else if (state.isStashNominating) {
onUpdateType(stashId, 'nominator');
} else {
onUpdateType(stashId, 'other');
}
onUpdateType(
stashId,
state.isStashValidating
? 'validator'
: state.isStashNominating
? 'nominator'
: 'other'
);
}
}, [allAccounts, allStashes, onUpdateType, stakingAccount, stashId, validateInfo]);
@ -156,45 +132,8 @@ function Account ({ allStashes, className, isInElection, isOwnStash, next, onUpd
);
}, [inactiveNoms, nominees]);
useEffect((): void => {
rewards && setStakingRewards([
rewards,
rewards.map(({ era }): EraIndex => era),
rewards.reduce((result, { total }) => result.iadd(total), new BN(0))
]);
}, [rewards]);
const _doPayout = useCallback(
(): void => queueExtrinsic({
accountId: controllerId,
extrinsic: createPayout(api, payoutRewards)
}),
[api, controllerId, payoutRewards, queueExtrinsic]
);
return (
<tr className={className}>
{api.query.staking.activeEra && (
<td>
{!rewards
? <Spinner variant='mini' />
: !!payoutEras.length && (
<Badge
hover={
<>
<div>{t('Pending payouts for {{count}} eras:', { replace: { count: payoutEras.length } })}</div>
<FormatBalance value={payoutTotal} />
</>
}
info={payoutEras.length}
isInline
isTooltip
type='counter'
/>
)
}
</td>
)}
<td className='address'>
<BondExtra
controllerId={controllerId}
@ -202,6 +141,17 @@ function Account ({ allStashes, className, isInElection, isOwnStash, next, onUpd
onClose={toggleBondExtra}
stashId={stashId}
/>
{controllerId && (
<Nominate
controllerId={controllerId}
isOpen={isNominateOpen}
next={next}
nominees={nominees}
onClose={toggleNominate}
stashId={stashId}
validators={validators}
/>
)}
<Unbond
controllerId={controllerId}
isOpen={isUnbondOpen}
@ -218,16 +168,6 @@ function Account ({ allStashes, className, isInElection, isOwnStash, next, onUpd
{isInjectOpen && (
<InjectKeys onClose={toggleInject} />
)}
{isNominateOpen && controllerId && (
<Nominate
controllerId={controllerId}
next={next}
nominees={nominees}
onClose={toggleNominate}
stakingOverview={stakingOverview}
stashId={stashId}
/>
)}
{isSetControllerOpen && (
<SetControllerAccount
defaultControllerId={controllerId}
@ -371,20 +311,6 @@ function Account ({ allStashes, className, isInElection, isOwnStash, next, onUpd
text
vertical
>
{api.query.staking.activeEra && (
<Menu.Item
disabled={payoutEras.length === 0}
onClick={_doPayout}
>
<Trans i18nKey='payoutEras'>
{t('Payout reward')}&nbsp;{
payoutEras.length
? <>(<FormatBalance value={payoutTotal} />)</>
: ''
}
</Trans>
</Menu.Item>
)}
<Menu.Item
disabled={!isOwnStash && !balancesAll?.freeBalance.gtn(0)}
onClick={toggleBondExtra}

View File

@ -2,7 +2,6 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DeriveStakingOverview, DeriveStakerReward } from '@polkadot/api-derive/types';
import { ActiveEraInfo, ElectionStatus, EraIndex } from '@polkadot/types/interfaces';
import React, { useCallback, useEffect, useState } from 'react';
@ -10,20 +9,18 @@ import { Table } from '@polkadot/react-components';
import { useCall, useApi, useOwnStashes } from '@polkadot/react-hooks';
import { Option } from '@polkadot/types';
import { useTranslation } from '../translate';
import Account from './Account';
import NewStake from './NewStake';
import { useTranslation } from '../translate';
interface Props {
allRewards?: Record<string, DeriveStakerReward[]>;
allStashes?: string[];
className?: string;
isVisible: boolean;
next?: string[];
stakingOverview?: DeriveStakingOverview;
validators?: string[];
}
function Actions ({ allRewards, allStashes, className, isVisible, next, stakingOverview }: Props): React.ReactElement<Props> {
function Actions ({ allStashes, className, next, validators }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const activeEra = useCall<EraIndex | undefined>(api.query.staking?.activeEra, [], {
@ -58,7 +55,7 @@ function Actions ({ allRewards, allStashes, className, isVisible, next, stakingO
);
return (
<div className={`${className} ${!isVisible && 'staking--hidden'}`}>
<div className={className}>
<NewStake isInElection={isInElection} />
{isInElection && (
<article className='warning nomargin'>
@ -68,7 +65,7 @@ function Actions ({ allRewards, allStashes, className, isVisible, next, stakingO
<Table
empty={t('No funds staked yet. Bond funds to validate or nominate a validator')}
header={[
[t('stashes'), 'start', 2],
[t('stashes'), 'start'],
[t('controller'), 'address'],
[t('rewards'), 'number'],
[t('bonded'), 'number'],
@ -84,9 +81,8 @@ function Actions ({ allRewards, allStashes, className, isVisible, next, stakingO
key={stashId}
next={next}
onUpdateType={_onUpdateType}
rewards={allRewards && allRewards[stashId]}
stakingOverview={stakingOverview}
stashId={stashId}
validators={validators}
/>
))}
</Table>

View File

@ -12,7 +12,6 @@ import { AddressSmall, Icon } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { FormatBalance } from '@polkadot/react-query';
import keyring from '@polkadot/ui-keyring';
import { formatNumber } from '@polkadot/util';
import Favorite from './Favorite';
import Status from './Status';
@ -30,7 +29,7 @@ interface Props {
lastBlock?: string;
onlineCount?: false | number;
onlineMessage?: boolean;
points?: false | number;
points?: string;
setNominators?: false | ((nominators: string[]) => void);
toggleFavorite: (accountId: string) => void;
}
@ -161,7 +160,7 @@ function Address ({ address, className, filterName, hasQueries, isAuthor, isElec
{commission}
</td>
<td className='number'>
{points && formatNumber(points)}
{points}
</td>
<td className='number'>
{lastBlock}

View File

@ -4,24 +4,22 @@
import { DeriveHeartbeats, DeriveStakingOverview } from '@polkadot/api-derive/types';
import { AccountId } from '@polkadot/types/interfaces';
import { AddressDetails } from './types';
import { Authors } from '@polkadot/react-query/BlockAuthors';
import React, { useCallback, useEffect, useReducer, useState } from 'react';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { Input, Table } from '@polkadot/react-components';
import { useFavorites } from '@polkadot/react-hooks';
import { useApi, useCall, useFavorites } from '@polkadot/react-hooks';
import { BlockAuthorsContext } from '@polkadot/react-query';
import { STORE_FAVS_BASE } from '../constants';
import { useTranslation } from '../translate';
import Address from './Address';
interface Props {
authorsMap: Record<string, string>;
hasQueries: boolean;
isIntentions?: boolean;
lastAuthors?: string[];
next?: string[];
recentlyOnline?: DeriveHeartbeats;
setNominators: (nominators: string[]) => void;
setNominators?: (nominators: string[]) => void;
stakingOverview?: DeriveStakingOverview;
}
@ -33,6 +31,8 @@ interface Filtered {
waiting?: AccountExtend[];
}
const EmptyAuthorsContext: React.Context<Authors> = React.createContext<Authors>({ byAuthor: {}, eraPoints: {}, lastBlockAuthors: [], lastHeaders: [] });
function filterAccounts (accounts: string[] = [], elected: string[], favorites: string[], without: string[]): AccountExtend[] {
return accounts
.filter((accountId): boolean => !without.includes(accountId as any))
@ -52,36 +52,6 @@ function accountsToString (accounts: AccountId[]): string[] {
return accounts.map((accountId): string => accountId.toString());
}
function reduceDetails (state: Record<string, AddressDetails>, _details: AddressDetails | AddressDetails[]): Record<string, AddressDetails> {
const details = Array.isArray(_details)
? _details
: [_details];
return details.reduce((result, details): Record<string, AddressDetails> => {
result[details.address] = {
...(state[details.address] || {}),
...details
};
return result;
}, { ...state });
}
function getDetails (stakingOverview: DeriveStakingOverview, 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].toNumber()
: undefined
};
});
}
function getFiltered (stakingOverview: DeriveStakingOverview, favorites: string[], next?: string[]): Filtered {
const allElected = accountsToString(stakingOverview.nextElected);
const validatorIds = accountsToString(stakingOverview.validators);
@ -96,22 +66,19 @@ function getFiltered (stakingOverview: DeriveStakingOverview, favorites: string[
};
}
function CurrentList ({ authorsMap, hasQueries, isIntentions, lastAuthors, next, recentlyOnline, setNominators, stakingOverview }: Props): React.ReactElement<Props> | null {
function CurrentList ({ hasQueries, isIntentions, next, setNominators, stakingOverview }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const { byAuthor, eraPoints, lastBlockAuthors } = useContext(isIntentions ? EmptyAuthorsContext : BlockAuthorsContext);
const recentlyOnline = useCall<DeriveHeartbeats>(!isIntentions && api.derive.imOnline?.receivedHeartbeats, []);
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE);
const [{ elected, validators, waiting }, setFiltered] = useState<Filtered>({});
const [nameFilter, setNameFilter] = useState<string>('');
const [addressDetails, dispatchDetails] = useReducer(reduceDetails, {});
useEffect((): void => {
if (stakingOverview) {
const filtered = getFiltered(stakingOverview, favorites, next);
setFiltered(filtered);
dispatchDetails(
getDetails(stakingOverview, filtered.validators)
);
}
stakingOverview && setFiltered(
getFiltered(stakingOverview, favorites, next)
);
}, [favorites, next, stakingOverview]);
const _renderRows = useCallback(
@ -121,20 +88,20 @@ function CurrentList ({ authorsMap, hasQueries, isIntentions, lastAuthors, next,
address={address}
filterName={nameFilter}
hasQueries={hasQueries}
isAuthor={lastAuthors && lastAuthors.includes(address)}
isAuthor={lastBlockAuthors.includes(address)}
isElected={isElected}
isFavorite={isFavorite}
isMain={isMain}
key={address}
lastBlock={authorsMap[address]}
onlineCount={isMain && recentlyOnline?.[address]?.blockCount.toNumber()}
onlineMessage={isMain && recentlyOnline?.[address]?.hasMessage}
points={isMain && addressDetails[address] && addressDetails[address].points}
setNominators={isIntentions && setNominators}
lastBlock={byAuthor[address]}
onlineCount={recentlyOnline?.[address]?.blockCount.toNumber()}
onlineMessage={recentlyOnline?.[address]?.hasMessage}
points={eraPoints[address]}
setNominators={setNominators}
toggleFavorite={toggleFavorite}
/>
)),
[addressDetails, authorsMap, hasQueries, isIntentions, lastAuthors, nameFilter, recentlyOnline, setNominators, toggleFavorite]
[byAuthor, eraPoints, hasQueries, lastBlockAuthors, nameFilter, recentlyOnline, setNominators, toggleFavorite]
);
return isIntentions

View File

@ -2,35 +2,28 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DeriveHeartbeats, DeriveStakingOverview } from '@polkadot/api-derive/types';
import { DeriveStakingOverview } from '@polkadot/api-derive/types';
import { BareProps } from '@polkadot/react-components/types';
import React, { useContext } from 'react';
import { BlockAuthorsContext } from '@polkadot/react-query';
import React from 'react';
import CurrentList from './CurrentList';
interface Props extends BareProps {
hasQueries: boolean;
isIntentions?: boolean;
recentlyOnline?: DeriveHeartbeats;
next?: string[];
setNominators: (nominators: string[]) => void;
setNominators?: (nominators: string[]) => void;
stakingOverview?: DeriveStakingOverview;
}
function Overview ({ className, hasQueries, isIntentions, next, recentlyOnline, setNominators, stakingOverview }: Props): React.ReactElement<Props> {
const { byAuthor, lastBlockAuthors } = useContext(BlockAuthorsContext);
function Overview ({ className, hasQueries, isIntentions, next, setNominators, stakingOverview }: Props): React.ReactElement<Props> {
return (
<div className={`staking--Overview ${className}`}>
<CurrentList
authorsMap={byAuthor}
hasQueries={hasQueries}
isIntentions={isIntentions}
lastAuthors={lastBlockAuthors}
next={next}
recentlyOnline={recentlyOnline}
setNominators={setNominators}
stakingOverview={stakingOverview}
/>

View File

@ -0,0 +1,78 @@
// 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 { SubmittableExtrinsic } from '@polkadot/api/types';
import BN from 'bn.js';
import React, { useState, useEffect } from 'react';
import { Button, Modal, InputAddress, TxButton } from '@polkadot/react-components';
import { useApi, useToggle } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
interface Props {
eras: BN[];
isInElection?: boolean;
validatorId: string;
}
function PayButton ({ eras, isInElection, validatorId }: Props): React.ReactElement<Props> {
const { api } = useApi();
const { t } = useTranslation();
const [isVisible, togglePayout] = useToggle();
const [accountId, setAccount] = useState<string | null>(null);
const [extrinsic, setExtrinsic] = useState<SubmittableExtrinsic<'promise'> | null>(null);
useEffect((): void => {
setExtrinsic(
() => eras.length === 1
? api.tx.staking.payoutStakers(validatorId, eras[0])
: api.tx.utility.batch(
eras.map((era): SubmittableExtrinsic<'promise'> =>
api.tx.staking.payoutStakers(validatorId, era)
)
)
);
}, [api, eras, validatorId]);
return (
<>
{isVisible && (
<Modal header={t('Payout all stakers')}>
<Modal.Content>
<InputAddress
isDisabled
label={t('payout stakers for')}
value={validatorId}
/>
<InputAddress
label={t('request from')}
onChange={setAccount}
type='account'
value={accountId}
/>
</Modal.Content>
<Modal.Actions onCancel={togglePayout}>
<TxButton
accountId={accountId}
extrinsic={extrinsic}
icon='credit card outline'
isDisabled={!extrinsic || !accountId}
label={t('Payout')}
onStart={togglePayout}
/>
</Modal.Actions>
</Modal>
)}
<Button
icon='percent'
isDisabled={isInElection}
label={t('Payout')}
onClick={togglePayout}
/>
</>
);
}
export default React.memo(PayButton);

View File

@ -0,0 +1,101 @@
// 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 { SubmittableExtrinsic } from '@polkadot/api/types';
import { DeriveStakerReward, DeriveStakingAccount } from '@polkadot/api-derive/types';
import { PayoutStash } from './types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import ApiPromise from '@polkadot/api/promise';
import { AddressMini, Badge, TxButton } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { FormatBalance } from '@polkadot/react-query';
import { useTranslation } from '../translate';
import { createErasString } from './util';
interface Props {
className?: string;
isInElection?: boolean;
payout: PayoutStash;
stakerPayoutsAfter: BN;
}
function createPrevPayoutType (api: ApiPromise, { era, isValidator, nominating }: DeriveStakerReward): SubmittableExtrinsic<'promise'> {
return isValidator
? api.tx.staking.payoutValidator(era)
: api.tx.staking.payoutNominator(era, nominating.map(({ validatorId, validatorIndex }): [string, number] =>
[validatorId, validatorIndex]
));
}
function createPrevPayout (api: ApiPromise, payoutRewards: DeriveStakerReward[]): SubmittableExtrinsic<'promise'> {
return payoutRewards.length === 1
? createPrevPayoutType(api, payoutRewards[0])
: api.tx.utility.batch(payoutRewards.map((reward) => createPrevPayoutType(api, reward)));
}
function Stash ({ className, isInElection, payout: { available, rewards, stashId }, stakerPayoutsAfter }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const [extrinsic, setExtrinsic] = useState<SubmittableExtrinsic<'promise'> | null>(null);
const [eraStr, setEraStr] = useState('');
const stakingAccount = useCall<DeriveStakingAccount>(api.derive.staking.account as any, [stashId]);
useEffect((): void => {
rewards && setEraStr(
createErasString(rewards.map(({ era }): BN => era))
);
}, [rewards]);
useEffect((): void => {
if (rewards) {
const available = rewards.filter(({ era }) => era.lt(stakerPayoutsAfter));
setExtrinsic(
available.length
? createPrevPayout(api, available)
: null
);
}
}, [api, rewards, stakerPayoutsAfter]);
if (available.isZero()) {
return null;
}
return (
<tr className={className}>
<td className='address'><AddressMini value={stashId} /></td>
<td className='start'>
<Badge
info={rewards.length}
isInline
type='counter'
/>
<span className='payout-eras'>{eraStr}</span>
</td>
<td className='number'><FormatBalance value={available} /></td>
<td
className='button'
colSpan={3}
>
{extrinsic && stakingAccount && (
<TxButton
accountId={stakingAccount.controllerId}
extrinsic={extrinsic}
icon='credit card outline'
isDisabled={!extrinsic || isInElection}
isPrimary={false}
label={t('Payout')}
withSpinner={false}
/>
)}
</td>
</tr>
);
}
export default React.memo(Stash);

View File

@ -0,0 +1,94 @@
// 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 { PayoutValidator } from './types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import { AddressMini, Badge, Expander } from '@polkadot/react-components';
import { FormatBalance } from '@polkadot/react-query';
import { useTranslation } from '../translate';
import PayButton from './PayButton';
import { createErasString } from './util';
interface Props {
className?: string;
isInElection?: boolean;
payout: PayoutValidator;
}
interface State {
eraStr: string;
eras: BN[];
nominators: Record<string, BN>;
numNominators: number;
}
function Payout ({ className, isInElection, payout }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [{ eraStr, eras, nominators, numNominators }, setState] = useState<State>({
eraStr: '',
eras: [],
nominators: {},
numNominators: 0
});
useEffect((): void => {
const eras = payout.eras.map(({ era }) => era);
const eraStr = createErasString(eras);
const nominators = payout.eras.reduce((nominators: Record<string, BN>, { stashes }): Record<string, BN> => {
Object.entries(stashes).forEach(([stashId, value]): void => {
if (nominators[stashId]) {
nominators[stashId] = nominators[stashId].add(value);
} else {
nominators[stashId] = value;
}
});
return nominators;
}, {});
setState({ eraStr, eras, nominators, numNominators: Object.keys(nominators).length });
}, [payout]);
return (
<tr className={className}>
<td className='address'><AddressMini value={payout.validatorId} /></td>
<td className='start'>
<Badge
info={payout.eras.length}
isInline
type='counter'
/>
<span className='payout-eras'>{eraStr}</span>
</td>
<td className='number'><FormatBalance value={payout.available} /></td>
<td
className='start'
colSpan={2}
>
<Expander summary={t('{{count}} stakers', { replace: { count: numNominators } })}>
{Object.entries(nominators).map(([stashId, balance]) =>
<AddressMini
balance={balance}
key={stashId}
value={stashId}
withBalance
/>
)}
</Expander>
</td>
<td className='button'>
<PayButton
eras={eras}
isInElection={isInElection}
validatorId={payout.validatorId}
/>
</td>
</tr>
);
}
export default React.memo(Payout);

View File

@ -0,0 +1,147 @@
// 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 { DeriveStakerReward } from '@polkadot/api-derive/types';
import { PayoutStash, PayoutValidator } from './types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { Table } from '@polkadot/react-components';
import { useApi, useOwnEraRewards } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
import useStakerPayouts from './useStakerPayouts';
import Stash from './Stash';
import Validator from './Validator';
interface Props {
className?: string;
isInElection?: boolean;
stakerPayoutsAfter: BN;
}
interface Available {
stashes?: PayoutStash[];
validators?: PayoutValidator[];
}
function groupByValidator (allRewards: Record<string, DeriveStakerReward[]>): PayoutValidator[] {
return Object
.entries(allRewards)
.reduce((grouped: PayoutValidator[], [stashId, rewards]): PayoutValidator[] => {
rewards.forEach((reward: DeriveStakerReward): void => {
Object
.entries(reward.validators)
.forEach(([validatorId, { value }]): void => {
const entry = grouped.find((entry) => entry.validatorId === validatorId);
if (entry) {
const eraEntry = entry.eras.find((entry) => entry.era.eq(reward.era));
if (eraEntry) {
eraEntry.stashes[stashId] = value;
} else {
entry.eras.push({
era: reward.era,
stashes: { [stashId]: value }
});
}
entry.available = entry.available.add(value);
} else {
grouped.push({
available: value,
eras: [{
era: reward.era,
stashes: { [stashId]: value }
}],
validatorId
});
}
});
});
return grouped;
}, [])
.sort((a, b) => b.available.cmp(a.available));
}
function extractStashes (allRewards: Record<string, DeriveStakerReward[]>): PayoutStash[] {
return Object
.entries(allRewards)
.map(([stashId, rewards]): PayoutStash => ({
available: rewards.reduce((result, { total }) => result.iadd(total), new BN(0)),
rewards,
stashId
}))
.filter(({ available }) => !available.isZero())
.sort((a, b) => b.available.cmp(a.available));
}
function Payouts ({ className, isInElection }: Props): React.ReactElement<Props> {
const { api } = useApi();
const [{ stashes, validators }, setPayouts] = useState<Available>({});
const stakerPayoutsAfter = useStakerPayouts();
const { allRewards } = useOwnEraRewards();
const { t } = useTranslation();
useEffect((): void => {
allRewards && setPayouts({
stashes: extractStashes(allRewards),
validators: groupByValidator(allRewards)
});
}, [allRewards]);
return (
<div className={className}>
<Table
empty={stashes && t('No pending payouts for your stashes')}
header={[
[t('payout/stash'), 'start'],
[t('eras'), 'start'],
[t('available')],
[undefined, undefined, 3]
]}
isFixed
>
{stashes?.map((payout): React.ReactNode => (
<Stash
isInElection={isInElection}
key={payout.stashId}
payout={payout}
stakerPayoutsAfter={stakerPayoutsAfter}
/>
))}
</Table>
{api.tx.staking.payoutStakers && (
<Table
empty={validators && t('No pending era payouts from validators')}
header={[
[t('payout/validator'), 'start'],
[t('eras'), 'start'],
[t('total')],
[undefined, undefined, 3]
]}
isFixed
>
{validators?.map((payout): React.ReactNode => (
<Validator
isInElection={isInElection}
key={payout.validatorId}
payout={payout}
/>
))}
</Table>
)}
</div>
);
}
export default React.memo(styled(Payouts)`
.payout-eras {
padding-left: 0.25rem;
vertical-align: middle;
}
`);

View File

@ -0,0 +1,25 @@
// 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 { DeriveStakerReward } from '@polkadot/api-derive/types';
import { Balance, EraIndex } from '@polkadot/types/interfaces';
import BN from 'bn.js';
export interface PayoutEraValidator {
era: EraIndex;
stashes: Record<string, Balance>;
}
export interface PayoutValidator {
available: BN;
eras: PayoutEraValidator[];
validatorId: string;
}
export interface PayoutStash {
available: BN;
rewards: DeriveStakerReward[];
stashId: string;
}

View File

@ -0,0 +1,26 @@
// 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 { EraIndex } from '@polkadot/types/interfaces';
import BN from 'bn.js';
import { useEffect, useState } from 'react';
import { useApi, useCall } from '@polkadot/react-hooks';
import { Option } from '@polkadot/types';
export default function useStakerPayouts (): BN {
const { api } = useApi();
const [stakerPayoutAfter, setState] = useState<BN>(
api.tx.staking.payoutStakers
? new BN(0)
: new BN(1_000_000_000)
);
const migrateEraOpt = useCall<Option<EraIndex>>(api.query.staking?.migrateEra, []);
useEffect((): void => {
migrateEraOpt?.isSome && setState(migrateEraOpt.unwrap());
}, [migrateEraOpt]);
return stakerPayoutAfter;
}

View File

@ -0,0 +1,47 @@
// 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 BN from 'bn.js';
import { formatNumber } from '@polkadot/util';
function isSingle (entry: BN | [BN, BN]): entry is BN {
return !Array.isArray(entry);
}
export function createErasString (eras: BN[]): string {
if (!eras.length) {
return '';
}
return eras
.reduce((result: (BN | [BN, BN])[], era): (BN | [BN, BN])[] => {
if (result.length === 0) {
return [era];
} else {
const last = result[result.length - 1];
if (isSingle(last)) {
if (last.addn(1).eq(era)) {
result[result.length - 1] = [last, era];
} else {
result.push(era);
}
} else {
if (last[1].addn(1).eq(era)) {
last[1] = era;
} else {
result.push(era);
}
}
}
return result;
}, [])
.map((entry): string =>
isSingle(entry)
? formatNumber(entry)
: `${formatNumber(entry[0])}-${formatNumber(entry[1])}`
)
.join(', ');
}

View File

@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DeriveHeartbeats, DeriveStakingOverview } from '@polkadot/api-derive/types';
import { DeriveStakingOverview } from '@polkadot/api-derive/types';
import { AppProps as Props } from '@polkadot/react-components/types';
import { AccountId } from '@polkadot/types/interfaces';
@ -12,17 +12,21 @@ import { useLocation } from 'react-router-dom';
import styled from 'styled-components';
import { HelpOverlay } from '@polkadot/react-components';
import Tabs from '@polkadot/react-components/Tabs';
import { useAccounts, useApi, useCall, useOwnEraRewards } from '@polkadot/react-hooks';
import { useAccounts, useApi, useCall } from '@polkadot/react-hooks';
import basicMd from './md/basic.md';
import Actions from './Actions';
import Overview from './Overview';
import Summary from './Overview/Summary';
import Payouts from './Payouts';
import Query from './Query';
import Summary from './Overview/Summary';
import Targets from './Targets';
import { useTranslation } from './translate';
export { default as useCounter } from './useCounter';
interface Validators {
next?: string[];
validators?: string[];
}
function reduceNominators (nominators: string[], additional: string[]): string[] {
return nominators.concat(...additional.filter((nominator): boolean => !nominators.includes(nominator)));
@ -33,13 +37,11 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement<Props>
const { api } = useApi();
const { hasAccounts } = useAccounts();
const { pathname } = useLocation();
const { allRewards, rewardCount } = useOwnEraRewards();
const [next, setNext] = useState<string[] | undefined>();
const [{ next, validators }, setValidators] = useState<Validators>({});
const allStashes = useCall<string[]>(api.derive.staking.stashes, [], {
transform: (stashes: AccountId[]): string[] =>
stashes.map((accountId): string => accountId.toString())
});
const recentlyOnline = useCall<DeriveHeartbeats>(api.derive.imOnline?.receivedHeartbeats, []);
const stakingOverview = useCall<DeriveStakingOverview>(api.derive.staking.overview, []);
const [nominators, dispatchNominators] = useReducer(reduceNominators, [] as string[]);
const hasQueries = useMemo(
@ -55,28 +57,28 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement<Props>
},
{
name: 'actions',
text: t('Account actions{{count}}', {
replace: {
count: rewardCount
? ` (${rewardCount})`
: ''
}
})
text: t('Account actions')
},
api.query.staking.activeEra
? {
name: 'payout',
text: 'Payouts'
}
: null,
{
name: 'calculator',
text: t('Calculator')
},
{
name: 'waiting',
text: t('Waiting')
},
{
name: 'returns',
text: t('Returns')
},
{
hasParams: true,
name: 'query',
text: t('Validator stats')
}
], [rewardCount, t]);
].filter((q): q is { name: string; text: string } => !!q), [api, t]);
const hiddenTabs = useMemo(
(): string[] =>
!hasAccounts
@ -88,11 +90,10 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement<Props>
);
useEffect((): void => {
allStashes && stakingOverview && setNext(
allStashes.filter((address): boolean =>
!stakingOverview.validators.includes(address as any)
)
);
allStashes && stakingOverview && setValidators({
next: allStashes.filter((address) => !stakingOverview.validators.includes(address as any)),
validators: stakingOverview.validators.map((a) => a.toString())
});
}, [allStashes, stakingOverview]);
return (
@ -112,35 +113,35 @@ function StakingApp ({ basePath, className }: Props): React.ReactElement<Props>
stakingOverview={stakingOverview}
/>
<Switch>
<Route path={`${basePath}/calculator`}>
<Targets />
</Route>
<Route path={`${basePath}/payout`}>
<Payouts />
</Route>
<Route path={[`${basePath}/query/:value`, `${basePath}/query`]}>
<Query />
</Route>
<Route path={`${basePath}/returns`}>
<Targets />
</Route>
<Route path={`${basePath}/waiting`}>
<Overview
className={`${basePath}/waiting` === pathname ? '' : 'staking--hidden'}
hasQueries={hasQueries}
isIntentions
next={next}
recentlyOnline={recentlyOnline}
setNominators={dispatchNominators}
stakingOverview={stakingOverview}
/>
</Route>
</Switch>
<Actions
allRewards={allRewards}
allStashes={allStashes}
isVisible={pathname === `${basePath}/actions`}
className={pathname === `${basePath}/actions` ? '' : 'staking--hidden'}
next={next}
stakingOverview={stakingOverview}
validators={validators}
/>
<Overview
className={basePath === pathname ? '' : 'staking--hidden'}
hasQueries={hasQueries}
next={next}
recentlyOnline={recentlyOnline}
setNominators={dispatchNominators}
stakingOverview={stakingOverview}
/>

View File

@ -5,7 +5,7 @@
import { useOwnEraRewards } from '@polkadot/react-hooks';
export default function useCounter (): number {
const { rewardCount } = useOwnEraRewards(true);
const { rewardCount } = useOwnEraRewards();
return rewardCount;
}

View File

@ -43,7 +43,7 @@ function StorageApp ({ basePath, className }: Props): React.ReactElement<Props>
export default React.memo(styled(StorageApp)`
.storage--actionrow {
align-items: center;
align-items: flex-start;
display: flex;
.button {
@ -68,6 +68,6 @@ export default React.memo(styled(StorageApp)`
.storage--actionrow-buttons {
flex: 0;
padding: 0 0.25rem;
padding: 0.625rem 0.25rem;
}
`);

View File

@ -1,4 +1,4 @@
// Copyright 2017-2020 @polkadot/app-toolbox authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

View File

@ -31,8 +31,8 @@
"homepage": "https://github.com/polkadot-js/ui/tree/master/packages/ui-reactive#readme",
"dependencies": {
"@babel/runtime": "^7.9.2",
"@polkadot/api": "^1.10.0-beta.7",
"@polkadot/extension-dapp": "^0.24.0-beta.12",
"@polkadot/api": "^1.10.0-beta.17",
"@polkadot/extension-dapp": "^0.24.0-beta.17",
"rxjs-compat": "^6.5.5"
}
}

View File

@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
// Copyright 2017-2020 @polkadot/react-query authors & contributors
// Copyright 2017-2020 @polkadot/react-api authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

View File

@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
// Copyright 2017-2020 @polkadot/react-query authors & contributors
// Copyright 2017-2020 @polkadot/react-api authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

View File

@ -11,12 +11,12 @@
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.9.2",
"@polkadot/keyring": "^2.7.1",
"@polkadot/keyring": "^2.8.0-beta.7",
"@polkadot/react-api": "0.40.0-beta.251",
"@polkadot/react-identicon": "^0.52.0-beta.24",
"@polkadot/react-identicon": "^0.52.0-beta.30",
"@polkadot/react-query": "0.40.0-beta.251",
"@polkadot/ui-keyring": "^0.52.0-beta.24",
"@polkadot/ui-settings": "^0.52.0-beta.24",
"@polkadot/ui-keyring": "^0.52.0-beta.30",
"@polkadot/ui-settings": "^0.52.0-beta.30",
"chart.js": "^2.9.3",
"codeflask": "^1.4.1",
"i18next": "^19.3.4",

View File

@ -6,7 +6,7 @@ import { DeriveAccountInfo, DeriveAccountRegistration } from '@polkadot/api-deri
import { BareProps } from '@polkadot/react-api/types';
import { AccountId, AccountIndex, Address } from '@polkadot/types/interfaces';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import registry from '@polkadot/react-api/typeRegistry';
import { useCall, useApi, useRegistrars, useToggle } from '@polkadot/react-hooks';
@ -23,6 +23,7 @@ interface Props extends BareProps {
children?: React.ReactNode;
defaultName?: string;
label?: React.ReactNode;
noName?: boolean;
onClick?: () => void;
override?: React.ReactNode;
// this is used by app-account/addresses to toggle editing
@ -172,19 +173,18 @@ function extractIdentity (address: string, identity: DeriveAccountRegistration,
);
}
function AccountName ({ children, className, defaultName, label, onClick, override, style, toggle, value }: Props): React.ReactElement<Props> {
function AccountName ({ children, className, defaultName, label, noName, onClick, override, toggle, value }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const { isRegistrar, registrars } = useRegistrars();
const { isRegistrar, registrars } = useRegistrars(noName);
const [isJudgementOpen, toggleJudgement] = useToggle();
const info = useCall<DeriveAccountInfo>(api.derive.accounts.info as any, [value]);
const address = useMemo((): string => (value || '').toString(), [value]);
const [name, setName] = useState<React.ReactNode>((): React.ReactNode => extractName((value || '').toString(), undefined, defaultName));
const info = useCall<DeriveAccountInfo>(!noName && api.derive.accounts.info as any, [value]);
const [name, setName] = useState<React.ReactNode>(() => extractName((value || '').toString(), undefined, defaultName));
// set the actual nickname, local name, accountIndex, accountId
useEffect((): void => {
const { accountId, accountIndex, identity, nickname } = info || {};
const cacheAddr = (accountId || address).toString();
const cacheAddr = (accountId || value || '').toString();
if (api.query.identity?.identityOf) {
setName((): React.ReactNode =>
@ -199,13 +199,13 @@ function AccountName ({ children, className, defaultName, label, onClick, overri
} else {
setName(defaultOrAddr(defaultName, cacheAddr, accountIndex));
}
}, [api, address, defaultName, info, isRegistrar, t, toggle, toggleJudgement]);
}, [api, defaultName, info, isRegistrar, t, toggle, toggleJudgement, value]);
return (
<>
{isJudgementOpen && (
<AccountNameJudgement
address={address}
address={(value || '').toString()}
registrars={registrars}
toggleJudgement={toggleJudgement}
/>
@ -217,7 +217,6 @@ function AccountName ({ children, className, defaultName, label, onClick, overri
? undefined
: onClick
}
style={style}
>
{label || ''}{override || name}{children}
</div>

View File

@ -26,6 +26,7 @@ interface Props extends BareProps {
isShort?: boolean;
label?: React.ReactNode;
labelBalance?: React.ReactNode;
noName?: boolean;
type?: KeyringItemType;
value?: AccountId | AccountIndex | Address | string | null | Uint8Array;
withAddress?: boolean;
@ -36,7 +37,7 @@ interface Props extends BareProps {
withShrink?: boolean;
}
function AddressMini ({ balance, bonded, children, className, iconInfo, isPadded = true, label, labelBalance, style, value, withAddress = true, withBalance = false, withBonded = false, withLockedVote = false, withName = true, withShrink = false }: Props): React.ReactElement<Props> | null {
function AddressMini ({ balance, bonded, children, className, iconInfo, isPadded = true, label, labelBalance, noName, style, value, withAddress = true, withBalance = false, withBonded = false, withLockedVote = false, withName = true, withShrink = false }: Props): React.ReactElement<Props> | null {
if (!value) {
return null;
}
@ -64,7 +65,12 @@ function AddressMini ({ balance, bonded, children, className, iconInfo, isPadded
{withAddress && (
<div className='ui--AddressMini-address'>
{withName
? <AccountName value={value} />
? (
<AccountName
noName={noName}
value={value}
/>
)
: toShortAddress(value)
}
</div>

View File

@ -1,8 +1,9 @@
/* eslint-disable @typescript-eslint/camelcase */
// Copyright 2017-2020 @polkadot/react-components authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
/* eslint-disable @typescript-eslint/camelcase */
import { DeriveAccountInfo, DeriveStakingAccount } from '@polkadot/api-derive/types';
import { ApiProps } from '@polkadot/react-api/types';
import { I18nProps } from '@polkadot/react-components/types';

View File

@ -17,6 +17,7 @@ interface Props {
className?: string;
isHidden?: boolean;
filter?: string;
noName?: boolean;
noToggle?: boolean;
onChange?: (isChecked: boolean) => void;
value?: boolean;
@ -45,9 +46,9 @@ function getIsFiltered (address: string, filter?: string, info?: DeriveAccountIn
return true;
}
function AddressToggle ({ address, className, filter, isHidden, noToggle, onChange, value }: Props): React.ReactElement<Props> | null {
function AddressToggle ({ address, className, filter, isHidden, noName, noToggle, onChange, value }: Props): React.ReactElement<Props> | null {
const { api } = useApi();
const info = useCall<DeriveAccountInfo>(api.derive.accounts.info as any, [address]);
const info = useCall<DeriveAccountInfo>(!noName && api.derive.accounts.info as any, [address]);
const [isFiltered, setIsFiltered] = useState(false);
useEffect((): void => {
@ -66,6 +67,7 @@ function AddressToggle ({ address, className, filter, isHidden, noToggle, onChan
>
<AddressMini
className='ui--AddressToggle-address'
noName={noName}
value={address}
/>
{!noToggle && (
@ -123,8 +125,6 @@ export default React.memo(styled(AddressToggle)`
}
&.isAye {
cursor: move;
.ui--AddressToggle-address {
filter: none;
opacity: 1;

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// Copyright 2017-2020 @polkadot/react-components authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
@ -69,7 +68,7 @@ function calculateOptions (aspectRatio: number, values: HorizBarValue[], jsonVal
},
tooltips: {
callbacks: {
label: (item: TooltipItem, _: any): string =>
label: (item: TooltipItem): string =>
values[item.index].tooltip || values[item.index].label
}
}

View File

@ -21,6 +21,7 @@ export interface Props extends BareProps {
isOpen?: boolean;
summary?: React.ReactNode;
summaryMeta?: Meta;
summarySub?: React.ReactNode;
withDot?: boolean;
withHidden?: boolean;
}
@ -38,7 +39,7 @@ function formatMeta (meta?: Meta): React.ReactNode | null {
: strings.slice(0, firstEmpty).join(' ');
}
function Expander ({ children, className, isOpen, summary, summaryMeta, withDot, withHidden }: Props): React.ReactElement<Props> {
function Expander ({ children, className, isOpen, summary, summaryMeta, summarySub, withDot, withHidden }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [isExpanded, toggleExpanded] = useToggle(isOpen);
const headerMain = useMemo(
@ -46,8 +47,8 @@ function Expander ({ children, className, isOpen, summary, summaryMeta, withDot,
[summary, summaryMeta]
);
const headerSub = useMemo(
() => summary ? formatMeta(summaryMeta) : null,
[summary, summaryMeta]
() => summary ? (formatMeta(summaryMeta) || summarySub) : null,
[summary, summaryMeta, summarySub]
);
const hasContent = useMemo(
(): boolean => !!children && (!Array.isArray(children) || children.length !== 0),

View File

@ -13,17 +13,13 @@ import uiSettings from '@polkadot/ui-settings';
import { ValidatorsContext } from '@polkadot/react-query';
import StatusContext from './Status/Context';
import Tooltip from './Tooltip';
import { useTranslation } from './translate';
export function getIdentityTheme (systemName: string): 'substrate' {
return ((uiSettings.icon === 'default' && getSystemIcon(systemName)) || uiSettings.icon) as 'substrate';
}
let id = 0;
function IdentityIcon ({ className, onCopy, prefix, size, theme, value }: Props): React.ReactElement<Props> {
const [trigger] = useState(`tooltip-identicon-${++id}`);
const { api, isApiReady, systemName } = useApi();
const { t } = useTranslation();
const info = useCall<DeriveAccountInfo>(isApiReady && api.derive.accounts.info as any, [value]);
@ -59,13 +55,8 @@ function IdentityIcon ({ className, onCopy, prefix, size, theme, value }: Props)
);
return (
<span
className={`ui--IdentityIcon-Outer ${className}`}
data-for={trigger}
data-tip={true}
>
<span className={`ui--IdentityIcon-Outer ${className}`}>
<BaseIdentityIcon
className={className}
isHighlight={isValidator}
onCopy={_onCopy}
prefix={prefix}
@ -73,11 +64,6 @@ function IdentityIcon ({ className, onCopy, prefix, size, theme, value }: Props)
theme={thisTheme as 'substrate'}
value={address}
/>
<Tooltip
className='address'
text={address}
trigger={trigger}
/>
</span>
);
}

View File

@ -1,221 +0,0 @@
// Copyright 2017-2020 @polkadot/react-components authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
/* eslint-disable @typescript-eslint/unbound-method */
import React, { useMemo, useState } from 'react';
import ReactDOM from 'react-dom';
import { DragDropContext, Droppable, Draggable, DraggableLocation, DraggableProvided, DraggableStateSnapshot, DroppableProvided, DropResult } from 'react-beautiful-dnd';
import styled from 'styled-components';
import { useDebounce } from '@polkadot/react-hooks';
// FIXME :()
import { PORTAL_ID } from '../../apps/src/Apps';
import { useTranslation } from './translate';
import AddressToggle from './AddressToggle';
import Input from './Input';
interface Props {
available: string[];
availableLabel: React.ReactNode;
className?: string;
help: React.ReactNode;
maxCount: number;
onChange: (values: string[]) => void;
valueLabel: React.ReactNode;
value: string[];
}
function uniquesOf (list: string[]): string[] {
return [...new Set(list)];
}
function InputAddressMulti ({ available: propsAvailable = [], className, help, maxCount, onChange, availableLabel, valueLabel, value }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [_filter, setFilter] = useState<string>('');
const filter = useDebounce(_filter);
const available = useMemo(
(): string[] => uniquesOf(propsAvailable),
[propsAvailable]
);
const isSelected = useMemo(
(): Record<string, boolean> => {
return available.reduce(
(result: Record<string, boolean>, address) => {
return {
...result,
[address]: value.includes(address)
};
},
{}
);
},
[value, available]
);
const onReorder = (source: DraggableLocation, destination: DraggableLocation): void => {
const result = Array.from(value);
const [removed] = result.splice(source.index, 1);
result.splice(destination.index, 0, removed);
onChange(uniquesOf(result));
};
const onSelect = (address: string): () => void => {
return (): void => {
if (isSelected[address] || (maxCount && value.length >= maxCount)) {
return;
}
onChange(
uniquesOf(
[
...value,
address
]
)
);
};
};
const onDeselect = (index: number): () => void => {
return (): void => {
onChange(
uniquesOf([
...value.slice(0, index),
...value.slice(index + 1)
])
);
};
};
const onDragEnd = (result: DropResult): void => {
const { destination, source } = result;
!!destination && onReorder(source, destination);
};
return (
<div className={`ui--InputAddressMulti ${className}`}>
<div className='ui--InputAddressMulti-column'>
<Input
autoFocus
className='ui--InputAddressMulti-Input label-small'
label={availableLabel}
onChange={setFilter}
placeholder={t('filter by name, address, or account index')}
value={_filter}
/>
<div className='ui--InputAddressMulti-items'>
{available.map((address): React.ReactNode => (
<AddressToggle
address={address}
filter={filter}
isHidden={isSelected[address]}
key={address}
noToggle
onChange={onSelect(address)}
/>
))}
</div>
</div>
<div className='ui--InputAddressMulti-column'>
<Input
autoFocus
className='ui--InputAddressMulti-Input label-small'
help={help}
inputClassName='retain-appearance'
isDisabled
label={valueLabel}
onChange={setFilter}
placeholder={t('drag and drop to reorder')}
value={''}
/>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId='available'>
{(provided: DroppableProvided): React.ReactElement => (
<div
className='ui--InputAddressMulti-items'
ref={provided.innerRef}
>
{value.map((address, index): React.ReactNode => (
<Draggable
draggableId={address}
index={index}
key={address}
>
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot): React.ReactElement => {
const element = (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<AddressToggle
address={address}
className={snapshot.isDragging ? 'isDragging' : ''}
noToggle
onChange={onDeselect(index)}
/>
</div>
);
if (snapshot.isDragging) {
return ReactDOM.createPortal(element, document.getElementById(PORTAL_ID) as Element);
}
return element;
}}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
</div>
);
}
export default React.memo(styled(InputAddressMulti)`
border-top-width: 0px;
margin-left: 2rem;
width: calc(100% - 2rem);
display: inline-flex;
justify-content: space-between;
.ui--InputAddressMulti-Input {
.ui.input {
margin-bottom: 0rem;
opacity: 1 !important;
input {
border-bottom-width: 0px;
border-bottom-right-radius: 0px;
border-bottom-left-radius: 0px;
}
}
}
.ui--InputAddressMulti-column {
display: flex;
flex-direction: column;
min-height: 15rem;
max-height: 15rem;
width: 50%;
padding: 0.25rem 0.5rem;
.ui--InputAddressMulti-items {
background: white;
border: 1px solid rgba(34,36,38,0.15);
border-top-width: 0;
border-radius: 0 0 0.286rem 0.286rem;
flex: 1;
overflow-y: auto;
}
}
`);

View File

@ -0,0 +1,37 @@
// Copyright 2017-2020 @polkadot/react-components 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 React, { useCallback } from 'react';
import AddressToggle from '../AddressToggle';
interface Props {
address: string;
filter: string;
isHidden?: boolean;
onSelect: (address: string) => void;
}
function Available ({ address, filter, isHidden, onSelect }: Props): React.ReactElement<Props> | null {
const _onSelect = useCallback(
(): void => onSelect(address),
[address, onSelect]
);
if (isHidden) {
return null;
}
return (
<AddressToggle
address={address}
filter={filter}
noName
noToggle
onChange={_onSelect}
/>
);
}
export default React.memo(Available);

View File

@ -0,0 +1,37 @@
// Copyright 2017-2020 @polkadot/react-components 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 React, { useCallback } from 'react';
import AddressToggle from '../AddressToggle';
interface Props {
address: string;
filter: string;
isHidden?: boolean;
onDeselect: (address: string) => void;
}
function Selected ({ address, filter, isHidden, onDeselect }: Props): React.ReactElement<Props> | null {
const _onDeselect = useCallback(
(): void => onDeselect(address),
[address, onDeselect]
);
if (isHidden) {
return null;
}
return (
<AddressToggle
address={address}
filter={filter}
noName
noToggle
onChange={_onDeselect}
/>
);
}
export default React.memo(Selected);

View File

@ -0,0 +1,52 @@
// Copyright 2017-2020 @polkadot/react-components 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 React from 'react';
import ReactDOM from 'react-dom';
import { Draggable, DraggableProvided, DraggableStateSnapshot } from 'react-beautiful-dnd';
import { PORTAL_ID } from '../../../apps/src/Apps';
import AddressToggle from '../AddressToggle';
interface Props {
address: string;
index: number;
onDeselect: (index: number) => void;
}
const portal = document.getElementById(PORTAL_ID) as Element;
function Selected ({ address, index, onDeselect }: Props): React.ReactElement<Props> {
return (
<Draggable
draggableId={address}
index={index}
key={address}
>
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot): React.ReactElement => {
const element = (
<div
// eslint-disable-next-line @typescript-eslint/unbound-method
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<AddressToggle
address={address}
className={snapshot.isDragging ? 'isDragging' : ''}
noToggle
onChange={onDeselect}
/>
</div>
);
return snapshot.isDragging
? ReactDOM.createPortal(element, portal)
: element;
}}
</Draggable>
);
}
export default React.memo(Selected);

View File

@ -0,0 +1,198 @@
// Copyright 2017-2020 @polkadot/react-components 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 React, { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { useDebounce } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
import Input from '../Input';
import Available from './Available';
import Selected from './Selected';
interface Props {
available: string[];
availableLabel: React.ReactNode;
className?: string;
defaultValue?: string[];
help: React.ReactNode;
maxCount: number;
onChange: (values: string[]) => void;
valueLabel: React.ReactNode;
}
// import { DragDropContext, Droppable, DraggableLocation, DroppableProvided, DropResult } from 'react-beautiful-dnd';
// <DragDropContext onDragEnd={_onDragEnd}>
// <Droppable droppableId='available'>
// {(provided: DroppableProvided): React.ReactElement => (
// <div
// className='ui--InputAddressMulti-items'
// // eslint-disable-next-line @typescript-eslint/unbound-method
// ref={provided.innerRef}
// >
// {value.map((address, index): React.ReactNode => (
// <Selected
// address={address}
// index={index}
// key={address}
// onDeselect={_onDeselect}
// />
// ))}
// {provided.placeholder}
// </div>
// )}
// </Droppable>
// </DragDropContext>
// const _onReorder = useCallback(
// (source: DraggableLocation, destination: DraggableLocation): void => {
// const result = Array.from(value);
// const [removed] = result.splice(source.index, 1);
// result.splice(destination.index, 0, removed);
// onChange(uniquesOf(result));
// },
// [onChange, value]
// );
// const _onDeselect = useCallback(
// (index: number): void =>
// onChange(
// uniquesOf([...value.slice(0, index), ...value.slice(index + 1)])
// ),
// [onChange, value]
// );
// const _onDragEnd = useCallback(
// (result: DropResult): void => {
// const { destination, source } = result;
// !!destination && _onReorder(source, destination);
// },
// [_onReorder]
// );
// NOTE Drag code above, disabled since it has massive performance implications
function InputAddressMulti ({ available, availableLabel, className, defaultValue, maxCount, onChange, valueLabel }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [_filter, setFilter] = useState<string>('');
const [selected, setSelected] = useState<string[]>([]);
const filter = useDebounce(_filter);
useEffect((): void => {
defaultValue && setSelected(defaultValue);
}, [defaultValue]);
useEffect((): void => {
selected && onChange(selected);
}, [onChange, selected]);
const _onSelect = useCallback(
(address: string): void =>
setSelected(
(selected: string[]) =>
!selected.includes(address) && (selected.length < maxCount)
? selected.concat(address)
: selected
),
[maxCount]
);
const _onDeselect = useCallback(
(address: string): void =>
setSelected(
(selected: string[]) =>
selected.includes(address)
? selected.filter((a) => a !== address)
: selected
),
[]
);
return (
<div className={`ui--InputAddressMulti ${className}`}>
<Input
autoFocus
className='ui--InputAddressMulti-Input label-small'
onChange={setFilter}
placeholder={t('filter by name, address, or account index')}
value={_filter}
/>
<div className='ui--InputAddressMulti-columns'>
<div className='ui--InputAddressMulti-column'>
<label>{valueLabel}</label>
<div className='ui--InputAddressMulti-items'>
{selected.map((address): React.ReactNode => (
<Selected
address={address}
filter={filter}
key={address}
onDeselect={_onDeselect}
/>
))}
</div>
</div>
<div className='ui--InputAddressMulti-column'>
<label>{availableLabel}</label>
<div className='ui--InputAddressMulti-items'>
{available.map((address): React.ReactNode => (
<Available
address={address}
filter={filter}
isHidden={selected?.includes(address)}
key={address}
onSelect={_onSelect}
/>
))}
</div>
</div>
</div>
</div>
);
}
export default React.memo(styled(InputAddressMulti)`
border-top-width: 0px;
margin-left: 2rem;
width: calc(100% - 2rem);
.ui--InputAddressMulti-Input {
.ui.input {
margin-bottom: 0.25rem;
opacity: 1 !important;
}
}
.ui--InputAddressMulti-columns {
display: inline-flex;
flex-direction: row-reverse;
justify-content: space-between;
width: 100%;
.ui--InputAddressMulti-column {
display: flex;
flex-direction: column;
min-height: 15rem;
max-height: 15rem;
width: 50%;
padding: 0.25rem 0.5rem;
.ui--InputAddressMulti-items {
padding: 0.5rem 0;
background: white;
border: 1px solid rgba(34,36,38,0.15);
border-radius: 0.286rem 0.286rem;
flex: 1;
overflow-y: auto;
.ui--AddressToggle {
padding-left: 0.75rem;
}
}
}
}
`);

View File

@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/camelcase */
// Copyright 2017-2020 @polkadot/react-components authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

View File

@ -12,22 +12,18 @@ interface Props {
children?: React.ReactNode;
className?: string;
empty?: React.ReactNode;
isEmpty?: boolean;
}
function Body ({ children, className, empty, isEmpty }: Props): React.ReactElement<Props> {
function Body ({ children, className, empty }: Props): React.ReactElement<Props> {
return (
<tbody className={className}>
{isEmpty
? (
<tr><td colSpan={100}>{
isString(empty)
? <div className='empty'>{empty}</div>
: empty || <Spinner />
}</td></tr>
)
: children
}
{children || (
<tr><td colSpan={100}>{
isString(empty)
? <div className='empty'>{empty}</div>
: empty || <Spinner />
}</td></tr>
)}
</tbody>
);
}

View File

@ -17,8 +17,19 @@ interface TableProps {
isFixed?: boolean;
}
function extractKids (children: React.ReactNode): [boolean, React.ReactNode] {
if (!Array.isArray(children)) {
return [!children, children];
}
const kids = children.filter((child) => !!child);
const isEmpty = kids.length === 0;
return [isEmpty, isEmpty ? null : kids];
}
function Table ({ children, className, empty, filter, header, isFixed }: TableProps): React.ReactElement<TableProps> {
const isEmpty = !children || (Array.isArray(children) && children.length === 0);
const [isEmpty, kids] = extractKids(children);
return (
<div className={`ui--Table ${className}`}>
@ -28,11 +39,8 @@ function Table ({ children, className, empty, filter, header, isFixed }: TablePr
header={header}
isEmpty={isEmpty}
/>
<Body
empty={empty}
isEmpty={isEmpty}
>
{children}
<Body empty={empty}>
{kids}
</Body>
</table>
</div>
@ -68,7 +76,7 @@ export default React.memo(styled(Table)`
}
&:last-child {
padding-right: 1.5rem;
padding-right: 0.75rem;
}
&.all {

View File

@ -86,11 +86,6 @@ export default css`
}
}
.ui.inverted.dimmer {
background-color: rgba(255, 255, 255, 0.75);
padding: 0 1rem 1rem;
}
.ui.label:not(.ui--Bubble) {
background: transparent;
font-weight: normal;
@ -126,11 +121,6 @@ export default css`
}
}
> :first-child:not(.icon) {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.description {
margin: 1.5em 0;
font-weight: 700;

View File

@ -2,12 +2,10 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DeriveSessionIndexes, DeriveStakingQuery, DeriveStakerReward } from '@polkadot/api-derive/types';
import { EraIndex } from '@polkadot/types/interfaces';
import { DeriveStakerReward } from '@polkadot/api-derive/types';
import { useEffect, useState } from 'react';
import BN from 'bn.js';
import useApi from './useApi';
import useCall from './useCall';
import useIsMountedRef from './useIsMountedRef';
@ -18,64 +16,31 @@ interface OwnRewards {
rewardCount: number;
}
function useNextPayouts (onlyLatest?: boolean): [string, BN][] | undefined {
const { api, isApiReady } = useApi();
const mountedRef = useIsMountedRef();
const stashIds = useOwnStashIds();
const allInfo = useCall<DeriveStakingQuery[]>(isApiReady && stashIds && api.derive.staking?.queryMulti, stashIds);
const indexes = useCall<DeriveSessionIndexes>(isApiReady && api.derive.session?.indexes, []);
const [nextPayouts, setNextPayouts] = useState<[string, BN][] | undefined>();
function getRewards ([[stashIds], available]: [[string[]], DeriveStakerReward[][]]): OwnRewards {
const allRewards: Record<string, DeriveStakerReward[]> = {};
useEffect((): void => {
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)])
.filter((value): value is [string, EraIndex] => !!value[1]);
setNextPayouts(
onlyLatest
? lastPayouts
.filter(([, era]) => era.lte(prevEra))
.map(([stashId]) => [stashId, prevEra])
: lastPayouts
);
}
}, [allInfo, indexes, mountedRef, onlyLatest, stashIds]);
return nextPayouts;
}
function getRewards ([thesePayouts, theseRewards]: [[string, EraIndex][], DeriveStakerReward[][]], nextPayouts: [string, BN][]): OwnRewards {
const allRewards = theseRewards.reduce((result: Record<string, DeriveStakerReward[]>, rewards, index): Record<string, DeriveStakerReward[]> => {
const [stashId] = thesePayouts[index];
const nextPayout = nextPayouts.find(([thisId]) => thisId === stashId);
if (nextPayout) {
result[stashId] = rewards.filter(({ era, isEmpty }) => !isEmpty && era.gte(nextPayout[1]));
}
return result;
}, {});
stashIds.forEach((stashId, index): void => {
allRewards[stashId] = available[index];
});
return {
allRewards,
rewardCount: Object.values(allRewards).filter((rewards) => !!rewards.length).length
rewardCount: Object.values(allRewards).filter((rewards) => rewards.length !== 0).length
};
}
export default function useOwnEraRewards (onlyLatest?: boolean): OwnRewards {
export default function useOwnEraRewards (): 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 stashIds = useOwnStashIds();
const available = useCall<[[string[]], DeriveStakerReward[][]]>(stashIds && api.derive.staking?.stakerRewardsMulti as any, [stashIds], { withParams: true });
const [state, setState] = useState<OwnRewards>({ rewardCount: 0 });
useEffect((): void => {
mountedRef.current && available && nextPayouts && setState(
getRewards(available, nextPayouts)
mountedRef.current && available && setState(
getRewards(available)
);
}, [available, mountedRef, nextPayouts]);
}, [available, mountedRef]);
return state;
}

View File

@ -14,12 +14,13 @@ import useCall from './useCall';
interface State {
isRegistrar: boolean;
registrars: (string | null)[];
skipQuery?: boolean;
}
export default function useRegistrars (): State {
export default function useRegistrars (skipQuery?: boolean): State {
const { api } = useApi();
const { allAccounts, hasAccounts } = useAccounts();
const query = useCall<Option<RegistrarInfo>[]>(hasAccounts && api.query.identity?.registrars, []);
const query = useCall<Option<RegistrarInfo>[]>(!skipQuery && hasAccounts && api.query.identity?.registrars, []);
const [state, setState] = useState<State>({ isRegistrar: false, registrars: [] });
// determine if we have a registrar or not - registrars are allowed to approve

View File

@ -2,14 +2,17 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { EraRewardPoints } from '@polkadot/types/interfaces';
import React, { useEffect, useState } from 'react';
import { HeaderExtended } from '@polkadot/api-derive';
import { useApi } from '@polkadot/react-hooks';
import { useApi, useCall } from '@polkadot/react-hooks';
import { formatNumber } from '@polkadot/util';
interface Authors {
export interface Authors {
byAuthor: Record<string, string>;
lastBlockAuthors?: string[];
eraPoints: Record<string, string>;
lastBlockAuthors: string[];
lastBlockNumber?: string;
lastHeader?: HeaderExtended;
lastHeaders: HeaderExtended[];
@ -22,12 +25,14 @@ interface Props {
const MAX_HEADERS = 25;
const byAuthor: Record<string, string> = {};
const BlockAuthorsContext: React.Context<Authors> = React.createContext<Authors>({ byAuthor, lastHeaders: [] });
const eraPoints: Record<string, string> = {};
const BlockAuthorsContext: React.Context<Authors> = React.createContext<Authors>({ byAuthor, eraPoints, lastBlockAuthors: [], lastHeaders: [] });
const ValidatorsContext: React.Context<string[]> = React.createContext<string[]>([]);
function BlockAuthorsBase ({ children }: Props): React.ReactElement<Props> {
const { api } = useApi();
const [state, setState] = useState<Authors>({ byAuthor, lastHeaders: [] });
const { api, isApiReady } = useApi();
const queryPoints = useCall<EraRewardPoints>(isApiReady && api.derive.staking.currentPoints, []);
const [state, setState] = useState<Authors>({ byAuthor, eraPoints, lastBlockAuthors: [], lastHeaders: [] });
const [validators, setValidators] = useState<string[]>([]);
useEffect((): void => {
@ -70,13 +75,19 @@ function BlockAuthorsBase ({ children }: Props): React.ReactElement<Props> {
}, [lastHeader])
.sort((a, b): number => b.number.unwrap().cmp(a.number.unwrap()));
setState({ byAuthor, lastBlockAuthors: lastBlockAuthors.slice(), lastBlockNumber, lastHeader, lastHeaders });
setState({ byAuthor, eraPoints, lastBlockAuthors: lastBlockAuthors.slice(), lastBlockNumber, lastHeader, lastHeaders });
}
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect((): void => {
queryPoints && [...queryPoints.individual.entries()].forEach(([accountId, points]): void => {
eraPoints[accountId.toString()] = formatNumber(points);
});
}, [queryPoints]);
return (
<ValidatorsContext.Provider value={validators}>
<BlockAuthorsContext.Provider value={state}>

View File

@ -12,6 +12,6 @@
"dependencies": {
"@babel/runtime": "^7.9.2",
"@polkadot/react-components": "0.40.0-beta.251",
"@polkadot/react-qr": "^0.52.0-beta.24"
"@polkadot/react-qr": "^0.52.0-beta.30"
}
}

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/camelcase */
// Copyright 2017-2020 @polkadot/react-signer authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-var-requires */
// Copyright 2017-2020 @polkadot/apps authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

View File

@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
// Copyright 2017-2019 @polkadot authors & contributors
// Copyright 2017-2020 @polkadot/apps authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

1564
yarn.lock

File diff suppressed because it is too large Load Diff