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:
parent
bc48e433b9
commit
d677d64817
@ -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');
|
||||
|
||||
@ -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'
|
||||
]
|
||||
});
|
||||
|
||||
18
package.json
18
package.json
@ -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",
|
||||
|
||||
@ -29,5 +29,6 @@ export default {
|
||||
Address: 'AccountId',
|
||||
LookupSource: 'AccountId',
|
||||
// previous substrate versions
|
||||
ReferendumInfo: 'ReferendumInfoTo239'
|
||||
ReferendumInfo: 'ReferendumInfoTo239',
|
||||
StakingLedger: 'StakingLedgerTo240'
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -10,5 +10,6 @@
|
||||
export default {
|
||||
Address: 'AccountId',
|
||||
Keys: 'SessionKeys5',
|
||||
LookupSource: 'AccountId'
|
||||
LookupSource: 'AccountId',
|
||||
StakingLedger: 'StakingLedgerTo240'
|
||||
};
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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()}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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')} {
|
||||
payoutEras.length
|
||||
? <>(<FormatBalance value={payoutTotal} />)</>
|
||||
: ''
|
||||
}
|
||||
</Trans>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item
|
||||
disabled={!isOwnStash && !balancesAll?.freeBalance.gtn(0)}
|
||||
onClick={toggleBondExtra}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
78
packages/page-staking/src/Payouts/PayButton.tsx
Normal file
78
packages/page-staking/src/Payouts/PayButton.tsx
Normal 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);
|
||||
101
packages/page-staking/src/Payouts/Stash.tsx
Normal file
101
packages/page-staking/src/Payouts/Stash.tsx
Normal 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);
|
||||
94
packages/page-staking/src/Payouts/Validator.tsx
Normal file
94
packages/page-staking/src/Payouts/Validator.tsx
Normal 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);
|
||||
147
packages/page-staking/src/Payouts/index.tsx
Normal file
147
packages/page-staking/src/Payouts/index.tsx
Normal 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;
|
||||
}
|
||||
`);
|
||||
25
packages/page-staking/src/Payouts/types.ts
Normal file
25
packages/page-staking/src/Payouts/types.ts
Normal 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;
|
||||
}
|
||||
26
packages/page-staking/src/Payouts/useStakerPayouts.ts
Normal file
26
packages/page-staking/src/Payouts/useStakerPayouts.ts
Normal 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;
|
||||
}
|
||||
47
packages/page-staking/src/Payouts/util.ts
Normal file
47
packages/page-staking/src/Payouts/util.ts
Normal 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(', ');
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
import { useOwnEraRewards } from '@polkadot/react-hooks';
|
||||
|
||||
export default function useCounter (): number {
|
||||
const { rewardCount } = useOwnEraRewards(true);
|
||||
const { rewardCount } = useOwnEraRewards();
|
||||
|
||||
return rewardCount;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`);
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
3
packages/react-api/test/enzyme.js
vendored
3
packages/react-api/test/enzyme.js
vendored
@ -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.
|
||||
|
||||
|
||||
3
packages/react-api/test/observable.js
vendored
3
packages/react-api/test/observable.js
vendored
@ -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.
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
`);
|
||||
@ -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);
|
||||
37
packages/react-components/src/InputAddressMulti/Selected.tsx
Normal file
37
packages/react-components/src/InputAddressMulti/Selected.tsx
Normal 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);
|
||||
@ -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);
|
||||
198
packages/react-components/src/InputAddressMulti/index.tsx
Normal file
198
packages/react-components/src/InputAddressMulti/index.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
@ -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.
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user