소스 검색

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
Jaco Greeff 4 년 전
부모
커밋
d677d64817
67개의 변경된 파일1401개의 추가작업 그리고 905개의 파일을 삭제
  1. 4 0
      babel.config.js
  2. 5 2
      jest.config.js
  3. 9 9
      package.json
  4. 2 1
      packages/apps-config/src/api/spec/centrifuge-chain.ts
  5. 4 2
      packages/apps-config/src/api/spec/edgeware.ts
  6. 2 1
      packages/apps-config/src/api/spec/kusama.ts
  7. 1 1
      packages/apps/package.json
  8. 2 2
      packages/page-accounts/package.json
  9. 0 1
      packages/page-claims/src/index.tsx
  10. 0 1
      packages/page-claims/src/util.ts
  11. 1 1
      packages/page-contracts/package.json
  12. 2 1
      packages/page-contracts/src/CodeRow.tsx
  13. 2 1
      packages/page-contracts/src/Codes/ValidateCode.tsx
  14. 3 2
      packages/page-council/src/Overview/Vote.tsx
  15. 5 1
      packages/page-democracy/src/Overview/Referendum.tsx
  16. 8 3
      packages/page-democracy/src/Overview/ReferendumVotes.tsx
  17. 0 1
      packages/page-explorer/src/BestHash.tsx
  18. 0 1
      packages/page-explorer/src/BlockInfo/ByNumber.tsx
  19. 0 1
      packages/page-explorer/src/Forks.tsx
  20. 2 1
      packages/page-staking/src/Actions/Account/BondExtra.tsx
  21. 13 21
      packages/page-staking/src/Actions/Account/Nominate.tsx
  22. 2 1
      packages/page-staking/src/Actions/Account/Unbond.tsx
  23. 25 99
      packages/page-staking/src/Actions/Account/index.tsx
  24. 6 10
      packages/page-staking/src/Actions/index.tsx
  25. 0 0
      packages/page-staking/src/Actions/useInactives.ts
  26. 2 3
      packages/page-staking/src/Overview/Address/index.tsx
  27. 21 54
      packages/page-staking/src/Overview/CurrentList.tsx
  28. 4 11
      packages/page-staking/src/Overview/index.tsx
  29. 78 0
      packages/page-staking/src/Payouts/PayButton.tsx
  30. 101 0
      packages/page-staking/src/Payouts/Stash.tsx
  31. 94 0
      packages/page-staking/src/Payouts/Validator.tsx
  32. 147 0
      packages/page-staking/src/Payouts/index.tsx
  33. 25 0
      packages/page-staking/src/Payouts/types.ts
  34. 26 0
      packages/page-staking/src/Payouts/useStakerPayouts.ts
  35. 47 0
      packages/page-staking/src/Payouts/util.ts
  36. 34 33
      packages/page-staking/src/index.tsx
  37. 1 1
      packages/page-staking/src/useCounter.ts
  38. 2 2
      packages/page-storage/src/index.tsx
  39. 1 1
      packages/page-toolbox/src/Rpc/Selection.tsx
  40. 2 2
      packages/react-api/package.json
  41. 1 2
      packages/react-api/test/enzyme.js
  42. 1 2
      packages/react-api/test/observable.js
  43. 4 4
      packages/react-components/package.json
  44. 9 10
      packages/react-components/src/AccountName.tsx
  45. 8 2
      packages/react-components/src/AddressMini.tsx
  46. 2 1
      packages/react-components/src/AddressRow.tsx
  47. 4 4
      packages/react-components/src/AddressToggle.tsx
  48. 1 2
      packages/react-components/src/Chart/HorizBar.tsx
  49. 4 3
      packages/react-components/src/Expander.tsx
  50. 1 15
      packages/react-components/src/IdentityIcon.tsx
  51. 0 221
      packages/react-components/src/InputAddressMulti.tsx
  52. 37 0
      packages/react-components/src/InputAddressMulti/Available.tsx
  53. 37 0
      packages/react-components/src/InputAddressMulti/Selected.tsx
  54. 52 0
      packages/react-components/src/InputAddressMulti/SelectedDrag.tsx
  55. 198 0
      packages/react-components/src/InputAddressMulti/index.tsx
  56. 1 1
      packages/react-components/src/InputBalanceBonded.tsx
  57. 8 12
      packages/react-components/src/Table/Body.tsx
  58. 15 7
      packages/react-components/src/Table/index.tsx
  59. 0 10
      packages/react-components/src/styles/semantic.ts
  60. 13 48
      packages/react-hooks/src/useOwnEraRewards.ts
  61. 3 2
      packages/react-hooks/src/useRegistrars.ts
  62. 18 7
      packages/react-query/src/BlockAuthors.tsx
  63. 1 1
      packages/react-signer/package.json
  64. 0 1
      packages/react-signer/src/Checks/index.tsx
  65. 0 1
      scripts/findPackages.js
  66. 1 2
      test/enzyme.js
  67. 299 276
      yarn.lock

+ 4 - 0
babel.config.js

@@ -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');

+ 5 - 2
jest.config.js

@@ -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'
   ]
 });

+ 9 - 9
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",

+ 2 - 1
packages/apps-config/src/api/spec/centrifuge-chain.ts

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

+ 4 - 2
packages/apps-config/src/api/spec/edgeware.ts

@@ -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

+ 2 - 1
packages/apps-config/src/api/spec/kusama.ts

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

+ 1 - 1
packages/apps/package.json

@@ -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"

+ 2 - 2
packages/page-accounts/package.json

@@ -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"
   }

+ 0 - 1
packages/page-claims/src/index.tsx

@@ -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.

+ 0 - 1
packages/page-claims/src/util.ts

@@ -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 - 1
packages/page-contracts/package.json

@@ -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"
   }
 }

+ 2 - 1
packages/page-contracts/src/CodeRow.tsx

@@ -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';
 

+ 2 - 1
packages/page-contracts/src/Codes/ValidateCode.tsx

@@ -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';

+ 3 - 2
packages/page-council/src/Overview/Vote.tsx

@@ -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>

+ 5 - 1
packages/page-democracy/src/Overview/Referendum.tsx

@@ -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}
       />

+ 8 - 3
packages/page-democracy/src/Overview/ReferendumVotes.tsx

@@ -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()}

+ 0 - 1
packages/page-explorer/src/BestHash.tsx

@@ -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.

+ 0 - 1
packages/page-explorer/src/BlockInfo/ByNumber.tsx

@@ -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.

+ 0 - 1
packages/page-explorer/src/Forks.tsx

@@ -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.

+ 2 - 1
packages/page-staking/src/Actions/Account/BondExtra.tsx

@@ -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';

+ 13 - 21
packages/page-staking/src/Actions/Account/Nominate.tsx

@@ -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>

+ 2 - 1
packages/page-staking/src/Actions/Account/Unbond.tsx

@@ -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';

+ 25 - 99
packages/page-staking/src/Actions/Account/index.tsx

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

+ 6 - 10
packages/page-staking/src/Actions/index.tsx

@@ -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>

+ 0 - 0
packages/page-staking/src/Actions/Account/useInactives.ts → packages/page-staking/src/Actions/useInactives.ts


+ 2 - 3
packages/page-staking/src/Overview/Address/index.tsx

@@ -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}

+ 21 - 54
packages/page-staking/src/Overview/CurrentList.tsx

@@ -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

+ 4 - 11
packages/page-staking/src/Overview/index.tsx

@@ -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 - 0
packages/page-staking/src/Payouts/PayButton.tsx

@@ -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 - 0
packages/page-staking/src/Payouts/Stash.tsx

@@ -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 - 0
packages/page-staking/src/Payouts/Validator.tsx

@@ -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 - 0
packages/page-staking/src/Payouts/index.tsx

@@ -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 - 0
packages/page-staking/src/Payouts/types.ts

@@ -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 - 0
packages/page-staking/src/Payouts/useStakerPayouts.ts

@@ -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 - 0
packages/page-staking/src/Payouts/util.ts

@@ -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(', ');
+}

+ 34 - 33
packages/page-staking/src/index.tsx

@@ -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: 'waiting',
-      text: t('Waiting')
+      name: 'calculator',
+      text: t('Calculator')
     },
     {
-      name: 'returns',
-      text: t('Returns')
+      name: 'waiting',
+      text: t('Waiting')
     },
     {
       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}
       />

+ 1 - 1
packages/page-staking/src/useCounter.ts

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

+ 2 - 2
packages/page-storage/src/index.tsx

@@ -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 - 1
packages/page-toolbox/src/Rpc/Selection.tsx

@@ -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.
 

+ 2 - 2
packages/react-api/package.json

@@ -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"
   }
 }

+ 1 - 2
packages/react-api/test/enzyme.js

@@ -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.
 

+ 1 - 2
packages/react-api/test/observable.js

@@ -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.
 

+ 4 - 4
packages/react-components/package.json

@@ -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",

+ 9 - 10
packages/react-components/src/AccountName.tsx

@@ -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>

+ 8 - 2
packages/react-components/src/AddressMini.tsx

@@ -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>

+ 2 - 1
packages/react-components/src/AddressRow.tsx

@@ -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';

+ 4 - 4
packages/react-components/src/AddressToggle.tsx

@@ -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 - 2
packages/react-components/src/Chart/HorizBar.tsx

@@ -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
         }
       }

+ 4 - 3
packages/react-components/src/Expander.tsx

@@ -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),

+ 1 - 15
packages/react-components/src/IdentityIcon.tsx

@@ -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>
   );
 }

+ 0 - 221
packages/react-components/src/InputAddressMulti.tsx

@@ -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;
-    }
-  }
-`);

+ 37 - 0
packages/react-components/src/InputAddressMulti/Available.tsx

@@ -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 - 0
packages/react-components/src/InputAddressMulti/Selected.tsx

@@ -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);

+ 52 - 0
packages/react-components/src/InputAddressMulti/SelectedDrag.tsx

@@ -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 - 0
packages/react-components/src/InputAddressMulti/index.tsx

@@ -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 - 1
packages/react-components/src/InputBalanceBonded.tsx

@@ -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.

+ 8 - 12
packages/react-components/src/Table/Body.tsx

@@ -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>
   );
 }

+ 15 - 7
packages/react-components/src/Table/index.tsx

@@ -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 {

+ 0 - 10
packages/react-components/src/styles/semantic.ts

@@ -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;

+ 13 - 48
packages/react-hooks/src/useOwnEraRewards.ts

@@ -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>();
-
-  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]);
+function getRewards ([[stashIds], available]: [[string[]], DeriveStakerReward[][]]): OwnRewards {
+  const allRewards: Record<string, DeriveStakerReward[]> = {};
 
-  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;
 }

+ 3 - 2
packages/react-hooks/src/useRegistrars.ts

@@ -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

+ 18 - 7
packages/react-query/src/BlockAuthors.tsx

@@ -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}>

+ 1 - 1
packages/react-signer/package.json

@@ -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"
   }
 }

+ 0 - 1
packages/react-signer/src/Checks/index.tsx

@@ -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.

+ 0 - 1
scripts/findPackages.js

@@ -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 - 2
test/enzyme.js

@@ -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.
 

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 299 - 276
yarn.lock


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.