Browse Source

Staking overview & actions (#171)

* Staking overview & actions

* Show session countdown

* Accounts with stake/Unstake buttons

* Small layout tweaks

* Cleanup multi-params from storage

* Submission/click works...

* Cleanups

* Render split

* Case sensitivity

* Doc links

* Icon to indicate validating
Jaco Greeff 6 years ago
parent
commit
cf8a24a8da

+ 1 - 0
README.md

@@ -23,6 +23,7 @@ The repo is split into a number of packages, each representing an application. T
 - [app-explorer](packages/app-explorer/) A simple block explorer. It only shows the most recent blocks, updating as they become available.
 - [app-extrinsics](packages/app-extrinsics/) Submission of extrinsics to a node.
 - [app-rpc](packages/app-rpc/) Sumission of raw data to RPC endpoints.
+- [app-staking](packages/app-staking/) A basic staking management app.
 - [app-storage](packages/app-storage/) A simple node storage query application. Multiple queries can be queued and updates as new values become available.
 - [app-toolbox](packages/app-toolbox/) Utilities to manage data.
 - [app-vanitygen](packages/app-vanitygen/) A toy that allows you to generate vanity addresses. Running `yarn run vanitygen --match <string>` runs the generator as a Node CLI app. (Orders of a magnitude faster due to the use of libsoldium bindings)

+ 1 - 1
jest.config.js

@@ -2,7 +2,7 @@ const config = require('@polkadot/dev-react/config/jest');
 
 module.exports = Object.assign({}, config, {
   moduleNameMapper: {
-    '@polkadot/app-(accounts|addresses|example|explorer|extrinsics|rpc|storage|toolbox|vanitygen)(.*)$': '<rootDir>/packages/ui-$1/src/$2',
+    '@polkadot/app-(accounts|addresses|example|explorer|extrinsics|rpc|staking|storage|toolbox|vanitygen)(.*)$': '<rootDir>/packages/ui-$1/src/$2',
     '@polkadot/ui-(app|identicon|keyring|react-rx|react|signer)(.*)$': '<rootDir>/packages/ui-$1/src/$2',
     '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'empty/object',
     '\\.(css|less)$': 'empty/object'

+ 14 - 5
packages/app-accounts/src/Address.tsx

@@ -2,7 +2,7 @@
 // This software may be modified and distributed under the terms
 // of the ISC license. See the LICENSE file for details.
 
-import { BareProps } from '@polkadot/ui-app/types';
+import { I18nProps } from '@polkadot/ui-app/types';
 
 import React from 'react';
 
@@ -14,7 +14,9 @@ import Nonce from '@polkadot/ui-react-rx/Nonce';
 import addressDecode from '@polkadot/util-keyring/address/decode';
 import addressEncode from '@polkadot/util-keyring/address/encode';
 
-type Props = BareProps & {
+import translate from './translate';
+
+type Props = I18nProps & {
   value: string;
 };
 
@@ -28,7 +30,7 @@ type State = {
 const DEFAULT_ADDR = '5'.padEnd(16, 'x');
 const DEFAULT_SHORT = `${DEFAULT_ADDR.slice(0, 7)}…${DEFAULT_ADDR.slice(-7)}`;
 
-export default class Address extends React.PureComponent<Props, State> {
+class Address extends React.PureComponent<Props, State> {
   state: State = {} as State;
 
   static getDerivedStateFromProps ({ value }: Props, { address, publicKey, shortValue }: State): State {
@@ -76,6 +78,7 @@ export default class Address extends React.PureComponent<Props, State> {
   }
 
   renderBalance () {
+    const { t } = this.props;
     const { isValid, publicKey } = this.state;
 
     if (!isValid) {
@@ -86,7 +89,9 @@ export default class Address extends React.PureComponent<Props, State> {
       <Balance
         className='accounts--Address-balance'
         key='balance'
-        label='balance '
+        label={t('address.balance', {
+          defaultValue: 'balance '
+        })}
         params={publicKey}
       />,
       <Nonce
@@ -94,8 +99,12 @@ export default class Address extends React.PureComponent<Props, State> {
         key='nonce'
         params={publicKey}
       >
-        {' transactions'}
+        {t('address.transactions', {
+          defaultValue: ' transactions'
+        })}
       </Nonce>
     ];
   }
 }
+
+export default translate(Address);

+ 15 - 0
packages/app-staking/LICENSE

@@ -0,0 +1,15 @@
+ISC License (ISC)
+
+Copyright 2017-2018 @polkadot/app-staking authors & contributors
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.

+ 1 - 0
packages/app-staking/README.md

@@ -0,0 +1 @@
+# @polkadot/app-staking

+ 21 - 0
packages/app-staking/package.json

@@ -0,0 +1,21 @@
+{
+  "name": "@polkadot/app-staking",
+  "version": "0.19.11",
+  "description": "A basic staking app",
+  "main": "index.js",
+  "scripts": {},
+  "author": "Jaco Greeff <jacogr@gmail.com>",
+  "maintainers": [
+    "Jaco Greeff <jacogr@gmail.com>"
+  ],
+  "license": "ISC",
+  "dependencies": {
+    "@babel/runtime": "^7.0.0-beta.51",
+    "@polkadot/storage": "^0.26.33",
+    "@polkadot/ui-app": "^0.19.9",
+    "@polkadot/ui-keyring": "^0.19.9",
+    "@polkadot/ui-react": "^0.19.9",
+    "@polkadot/ui-react-rx": "^0.19.9",
+    "@polkadot/util-keyring": "^0.26.33"
+  }
+}

+ 145 - 0
packages/app-staking/src/Account.tsx

@@ -0,0 +1,145 @@
+// Copyright 2017-2018 @polkadot/app-staking authors & contributors
+// This software may be modified and distributed under the terms
+// of the ISC license. See the LICENSE file for details.
+
+import { I18nProps } from '@polkadot/ui-app/types';
+import { Extrinsics } from '@polkadot/extrinsics/types';
+import { SectionItem } from '@polkadot/params/types';
+import { RawParam$Value } from '@polkadot/ui-app/Params/types';
+import { QueueTx$ExtrinsicAdd } from '@polkadot/ui-signer/types';
+
+import BN from 'bn.js';
+import React from 'react';
+import extrinsics from '@polkadot/extrinsics';
+import Button from '@polkadot/ui-app/Button';
+import Icon from '@polkadot/ui-app/Icon';
+import classes from '@polkadot/ui-app/util/classes';
+import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
+import RxBalance from '@polkadot/ui-react-rx/Balance';
+import RxNonce from '@polkadot/ui-react-rx/Nonce';
+import decodeAddress from '@polkadot/util-keyring/address/decode';
+
+import translate from './translate';
+
+type Props = I18nProps & {
+  address: string,
+  name: string,
+  nonce?: BN,
+  intentionPosition: number,
+  isIntending: boolean,
+  isValidator: boolean,
+  queueExtrinsic: QueueTx$ExtrinsicAdd
+};
+
+type State = {
+  nonce: BN;
+};
+
+class Account extends React.PureComponent<Props, State> {
+  constructor (props: Props) {
+    super(props);
+
+    this.state = {
+      nonce: new BN(0)
+    };
+  }
+  render () {
+    const { className, isIntending, style, t } = this.props;
+
+    return (
+      <div
+        className={classes('staking--Account', className)}
+        style={style}
+      >
+        {this.renderAccount()}
+        <Button
+          isDisabled={isIntending}
+          isPrimary
+          onClick={this.stake}
+          text={t('account.stake', {
+            defaultValue: 'stake'
+          })}
+        />
+        <Button
+          isDisabled={!isIntending}
+          isNegative
+          onClick={this.unstake}
+          text={t('account.unstake', {
+            defaultValue: 'unstake'
+          })}
+        />
+      </div>
+    );
+  }
+
+  private renderAccount () {
+    const { address, isValidator, name, t } = this.props;
+    const addrShort = `${address.slice(0, 7)}…${address.slice(-7)}`;
+
+    return (
+      <div className='staking--Account-details'>
+        <div>
+          <Icon
+            className={classes('staking--Account-validating', isValidator ? 'isValidator' : '')}
+            name='certificate'
+            size='large'
+          />
+          <IdentityIcon
+            className='staking--Account-icon'
+            size={32}
+            value={address}
+          />
+          <div className='staking--Account-info'>
+            <div className='staking--Account-name'>{name}</div>
+            <div className='staking--Account-address'>{addrShort}</div>
+          </div>
+        </div>
+        <RxBalance
+          className='staking--Account-balance'
+          label={t('account.balance', {
+            defaultValue: 'balance '
+          })}
+          params={address}
+        />
+        <RxNonce
+          className='staking--Account-nonce'
+          onChange={this.onChangeNonce}
+          params={address}
+        >
+          {t('account.transactions', {
+            defaultValue: ' transactions'
+          })}
+        </RxNonce>
+      </div>
+    );
+  }
+
+  private send (extrinsic: SectionItem<Extrinsics>, values: Array<RawParam$Value>) {
+    const { address, queueExtrinsic } = this.props;
+    const { nonce } = this.state;
+    const publicKey = decodeAddress(address);
+
+    queueExtrinsic({
+      extrinsic,
+      nonce,
+      publicKey,
+      values
+    });
+  }
+
+  private stake = () => {
+    this.send(extrinsics.staking.public.stake, []);
+  }
+
+  private unstake = () => {
+    const { intentionPosition } = this.props;
+
+    this.send(extrinsics.staking.public.unstake, [intentionPosition]);
+  }
+
+  private onChangeNonce = (nonce: BN) => {
+    this.setState({ nonce });
+  }
+}
+
+export default translate(Account);

+ 55 - 0
packages/app-staking/src/StakeList.tsx

@@ -0,0 +1,55 @@
+// Copyright 2017-2018 @polkadot/app-staking authors & contributors
+// This software may be modified and distributed under the terms
+// of the ISC license. See the LICENSE file for details.
+
+import { I18nProps } from '@polkadot/ui-app/types';
+import { QueueProps } from '@polkadot/ui-signer/types';
+
+import React from 'react';
+import classes from '@polkadot/ui-app/util/classes';
+import keyring from '@polkadot/ui-keyring/index';
+import { QueueConsumer } from '@polkadot/ui-signer/Context';
+
+import Account from './Account';
+import translate from './translate';
+
+type Props = I18nProps & {
+  intentions: Array<string>,
+  validators: Array<string>
+};
+
+class StakeList extends React.PureComponent<Props> {
+  render () {
+    const { className, intentions, style, validators } = this.props;
+
+    return (
+      <QueueConsumer>
+        {({ queueExtrinsic }: QueueProps) => (
+          <div
+            className={classes('staking--StakeList', className)}
+            style={style}
+          >
+            {keyring.getAccounts().map((account) => {
+              const address = account.address();
+              const name = account.getMeta().name || '';
+
+              return (
+                <Account
+                  address={address}
+                  intentionPosition={intentions.indexOf(address)}
+                  isIntending={intentions.includes(address)}
+                  isValidator={validators.includes(address)}
+                  key={address}
+                  name={name}
+                  queueExtrinsic={queueExtrinsic}
+                />
+              );
+            })}
+          </div>
+        )}
+      </QueueConsumer>
+    );
+  }
+}
+
+export default translate(StakeList);

+ 194 - 0
packages/app-staking/src/Summary.tsx

@@ -0,0 +1,194 @@
+// Copyright 2017-2018 @polkadot/app-staking authors & contributors
+// This software may be modified and distributed under the terms
+// of the ISC license. See the LICENSE file for details.
+
+import { Header } from '@polkadot/primitives/header';
+import { I18nProps } from '@polkadot/ui-app/types';
+import { ApiProps } from '@polkadot/ui-react-rx/types';
+
+import BN from 'bn.js';
+import React from 'react';
+import apimethods from '@polkadot/jsonrpc';
+import storage from '@polkadot/storage';
+import classes from '@polkadot/ui-app/util/classes';
+import withApiCall from '@polkadot/ui-react-rx/with/apiCall';
+import withMulti from '@polkadot/ui-react-rx/with/multi';
+import withStorage from '@polkadot/ui-react-rx/with/storage';
+
+import translate from './translate';
+
+type Props = ApiProps & I18nProps & {
+  intentions: Array<string>,
+  lastBlockHeader?: Header,
+  lastSessionChange?: BN,
+  sessionLength?: BN,
+  validators: Array<string>
+};
+
+type StateBalances = {
+  [index: string]: BN
+};
+
+type State = {
+  balances: StateBalances,
+  subscriptions: Array<any>
+};
+
+const DEFAULT_BALANCE = new BN(0);
+const DEFAULT_BLOCKNUMBER = new BN(0);
+const DEFAULT_SESSION_CHANGE = new BN(0);
+const DEFAULT_SESSION_LENGTH = new BN(60);
+
+class Summary extends React.PureComponent<Props, State> {
+  constructor (props: Props) {
+    super(props);
+
+    this.state = {
+      balances: {},
+      subscriptions: []
+    };
+  }
+
+  componentDidUpdate (prevProps: Props) {
+    const { intentions } = this.props;
+
+    if (intentions !== prevProps.intentions) {
+      this.subscribeBalances(intentions);
+    }
+  }
+
+  private subscribeBalances (accounts: string[]) {
+    const { api } = this.props;
+    const { balances, subscriptions } = this.state;
+    const newBalances = { ...balances };
+
+    accounts.forEach((account) => {
+      if (newBalances[account]) {
+        return;
+      } else {
+        newBalances[account] = DEFAULT_BALANCE;
+      }
+
+      subscriptions.push(
+        api.state
+          // Here we pass a parameter to the key generator, so it points to the correct storage entry
+          .getStorage(storage.staking.public.freeBalanceOf, account)
+          .subscribe((balance: BN) => {
+            this.setState(({ balances }: State) => {
+              const newBalances = { ...balances };
+
+              newBalances[account] = balance;
+
+              return {
+                balances: newBalances
+              } as State;
+            });
+          })
+      );
+    });
+
+    this.setState({
+      balances: newBalances,
+      subscriptions
+    } as State);
+  }
+
+  componentWillUnmount () {
+    const { subscriptions } = this.state;
+
+    subscriptions.forEach((sub) => sub.unsubscribe());
+  }
+
+  render () {
+    const { className, intentions, lastBlockHeader, lastSessionChange = DEFAULT_SESSION_CHANGE, style, sessionLength = DEFAULT_SESSION_LENGTH, t, validators } = this.props;
+    const blockNumber = lastBlockHeader
+      ? lastBlockHeader.number
+      : DEFAULT_BLOCKNUMBER;
+    const intentionHigh = this.calcIntentionsHigh();
+    const validatorLow = this.calcValidatorLow();
+
+    return (
+      <div
+        className={classes('staking--Summary', className)}
+        style={style}
+      >
+        <div>{t('summary.headline', {
+          defaultValue: '{{validatorCount}} validators, {{intentionCount}} accounts with intentions',
+          replace: {
+            intentionCount: intentions.length,
+            validatorCount: validators.length
+          }
+        })}</div>
+        <div>{t('summary.balance.validator', {
+          defaultValue: 'lowest validator balance is {{validatorLow}}',
+          replace: {
+            validatorLow: validatorLow ? validatorLow.toString() : 'unknown'
+          }
+        })}</div>
+        <div>{t('summary.balance.stake', {
+          defaultValue: ' highest balance intending to stake is {{intentionHigh}}',
+          replace: {
+            intentionHigh: intentionHigh ? intentionHigh.toString() : 'unknown'
+          }
+        })}</div>
+        <div>{t('summary.countdown', {
+          defaultValue: 'session block {{remainder}} / {{length}} at #{{blockNumber}}',
+          replace: {
+            blockNumber: blockNumber.toString(),
+            remainder: Math.max(1, blockNumber.sub(lastSessionChange).mod(sessionLength).addn(1).toNumber()).toString(),
+            length: sessionLength.toString()
+          }
+        })}</div>
+      </div>
+    );
+  }
+
+  private calcIntentionsHigh (): BN | null {
+    const { intentions, validators } = this.props;
+    const { balances } = this.state;
+
+    return intentions.reduce((high: BN | null, addr) => {
+      const balance = validators.includes(addr)
+        ? null
+        : balances[addr] || null;
+
+      if (high === null || (balance && high.lt(balance))) {
+        return balance;
+      }
+
+      return high;
+    }, null);
+  }
+
+  private calcValidatorLow (): BN | null {
+    const { validators } = this.props;
+    const { balances } = this.state;
+
+    return validators.reduce((low: BN | null, addr) => {
+      const balance = balances[addr] || null;
+
+      if (low === null || (balance && low.gt(balance))) {
+        return balance;
+      }
+
+      return low;
+    }, null);
+  }
+}
+
+export default withMulti(
+  Summary,
+  translate,
+  withApiCall(
+    apimethods.chain.public.newHead,
+    { propName: 'lastBlockHeader' }
+  ),
+  withStorage(
+    storage.session.public.length,
+    { propName: 'sessionLength' }
+  ),
+  withStorage(
+    storage.session.public.lastSessionChange,
+    { propName: 'lastSessionChange' }
+  )
+);

+ 58 - 0
packages/app-staking/src/index.css

@@ -0,0 +1,58 @@
+/* Copyright 2017-2018 @polkadot/app-staking authors & contributors
+/* This software may be modified and distributed under the terms
+/* of the ISC license. See the LICENSE file for details. */
+
+.staking--App {
+  padding: 1em 2rem;
+}
+
+.staking--Summary {
+  margin-bottom: 2rem;
+  text-align: right;
+}
+
+.staking--Account {
+  text-align: center;
+  margin-bottom: 1rem;
+}
+
+.staking--Account > .button {
+  margin-top: 1.25rem;
+  vertical-align: top;
+}
+
+.staking--Account-details {
+  display: inline-block;
+  margin-right: 1rem;
+  text-align: left;
+}
+
+.staking--Account-info {
+  display: inline-block;
+}
+
+.staking--Account-address {
+  font-family: monospace;
+  font-size: 1.5rem;
+}
+
+.staking--Account-icon {
+  margin-right: 1rem;
+}
+
+.staking--Account-validating {
+  opacity: 0.25 !important;
+  padding-top: 8px;
+  vertical-align: top !important;
+}
+
+.staking--Account-validating.isValidator {
+  color: red;
+  opacity: 1 !important;
+}
+
+.staking--Account-balance,
+.staking--Account-nonce {
+  opacity: 0.45;
+  text-align: right;
+}

+ 65 - 0
packages/app-staking/src/index.tsx

@@ -0,0 +1,65 @@
+// Copyright 2017-2018 @polkadot/app-staking authors & contributors
+// This software may be modified and distributed under the terms
+// of the ISC license. See the LICENSE file for details.
+
+import { BareProps } from '@polkadot/ui-app/types';
+
+import React from 'react';
+import storage from '@polkadot/storage';
+import encodeAddress from '@polkadot/util-keyring/address/encode';
+import classes from '@polkadot/ui-app/util/classes';
+import withStorage from '@polkadot/ui-react-rx/with/storage';
+import withMulti from '@polkadot/ui-react-rx/with/multi';
+
+import './index.css';
+
+import StakeList from './StakeList';
+import Summary from './Summary';
+
+type Props = BareProps & {
+  intentions?: Array<string>,
+  validators?: Array<string>
+};
+
+const transformAddress = (publicKeys: Array<Uint8Array>) =>
+  publicKeys.map(encodeAddress);
+
+class App extends React.PureComponent<Props> {
+  render () {
+    const { className, intentions = [], style, validators = [] } = this.props;
+
+    return (
+      <div
+        className={classes('staking--App', className)}
+        style={style}
+      >
+        <Summary
+          intentions={intentions}
+          validators={validators}
+        />
+        <StakeList
+          intentions={intentions}
+          validators={validators}
+        />
+      </div>
+    );
+  }
+}
+
+export default withMulti(
+  App,
+  withStorage(
+    storage.staking.public.intentions,
+    {
+      propName: 'intentions',
+      transform: transformAddress
+    }
+  ),
+  withStorage(
+    storage.session.public.validators,
+    {
+      propName: 'validators',
+      transform: transformAddress
+    }
+  )
+);

+ 7 - 0
packages/app-staking/src/translate.ts

@@ -0,0 +1,7 @@
+// Copyright 2017-2018 @polkadot/app-staking authors & contributors
+// This software may be modified and distributed under the terms
+// of the ISC license. See the LICENSE file for details.
+
+import { translate } from 'react-i18next';
+
+export default translate(['staking', 'ui']);

+ 1 - 0
packages/apps/package.json

@@ -23,6 +23,7 @@
     "@polkadot/app-explorer": "^0.19.11",
     "@polkadot/app-extrinsics": "^0.19.11",
     "@polkadot/app-rpc": "^0.19.11",
+    "@polkadot/app-staking": "^0.19.11",
     "@polkadot/app-storage": "^0.19.11",
     "@polkadot/app-toolbox": "^0.19.11",
     "@polkadot/app-vanitygen": "^0.19.11",

+ 2 - 1
packages/apps/src/routing/index.ts

@@ -12,6 +12,7 @@ import extrinsics from './extrinsics';
 // import home from './home';
 import rpc from './rpc';
 // import settings from './settings';
+import staking from './staking';
 import storage from './storage';
 import toolbox from './toolbox';
 import unknown from './unknown';
@@ -20,7 +21,7 @@ import vanitygen from './vanitygen';
 export default ({
   default: 'explorer',
   routes: ([] as Routes).concat(
-    example, explorer, extrinsics, storage, null, accounts, addresses, vanitygen, null, rpc, toolbox
+    example, explorer, staking, extrinsics, storage, null, accounts, addresses, vanitygen, null, rpc, toolbox
   ),
   unknown
 } as Routing);

+ 20 - 0
packages/apps/src/routing/staking.ts

@@ -0,0 +1,20 @@
+// Copyright 2017-2018 @polkadot/apps authors & contributors
+// This software may be modified and distributed under the terms
+// of the ISC license. See the LICENSE file for details.
+
+import { Routes } from '../types';
+
+import Staking from '@polkadot/app-staking/index';
+
+export default ([
+  {
+    Component: Staking,
+    i18n: {
+      defaultValue: 'Staking'
+    },
+    icon: 'certificate',
+    isExact: false,
+    isHidden: false,
+    name: 'staking'
+  }
+] as Routes);

+ 1 - 1
packages/apps/webpack.config.js

@@ -10,7 +10,7 @@ const CopyWebpackPlugin = require('copy-webpack-plugin');
 const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 const HtmlWebpackPlugin = require('html-webpack-plugin');
 
-const packages = ['app-accounts', 'app-addresses', 'app-example', 'app-explorer', 'app-extrinsics', 'app-rpc', 'app-storage', 'app-toolbox', 'app-vanitygen', 'ui-app', 'ui-identicon', 'ui-keyring', 'ui-react-rx', 'ui-react', 'ui-signer'];
+const packages = ['app-accounts', 'app-addresses', 'app-example', 'app-explorer', 'app-extrinsics', 'app-rpc', 'app-staking', 'app-storage', 'app-toolbox', 'app-vanitygen', 'ui-app', 'ui-identicon', 'ui-keyring', 'ui-react-rx', 'ui-react', 'ui-signer'];
 
 function createWebpack ({ alias = {}, context, name = 'index' }) {
   const pkgJson = require(path.join(context, 'package.json'));

+ 16 - 0
packages/ui-keyring/src/account/all.ts

@@ -0,0 +1,16 @@
+// Copyright 2017-2018 @polkadot/ui-keyring authors & contributors
+// This software may be modified and distributed under the terms
+// of the ISC license. See the LICENSE file for details.
+
+import { KeyringAddress, State } from '../types';
+
+import get from '../address/get';
+
+export default function all (state: State): Array<KeyringAddress> {
+  return Object
+    .keys(state.available.account)
+    .map((address) =>
+      get(state, address, 'account')
+    )
+    .filter((account) => !account.getMeta().isTesting);
+}

+ 3 - 3
packages/ui-keyring/src/address/get.ts

@@ -8,7 +8,7 @@ import isString from '@polkadot/util/is/string';
 import addressDecode from '@polkadot/util-keyring/address/decode';
 import addressEncode from '@polkadot/util-keyring/address/encode';
 
-export default function get (state: State, _address: string | Uint8Array): KeyringAddress {
+export default function get (state: State, _address: string | Uint8Array, type: 'account' | 'address' = 'address'): KeyringAddress {
   const address = isString(_address)
     ? _address
     : addressEncode(_address);
@@ -18,10 +18,10 @@ export default function get (state: State, _address: string | Uint8Array): Keyri
     address: (): string =>
       address,
     isValid: (): boolean =>
-      !!state.available.address[address],
+      !!state.available[type][address],
     publicKey: (): Uint8Array =>
       publicKey,
     getMeta: (): KeyringJson$Meta =>
-      state.available.address[address].meta
+      state.available[type][address].meta
   };
 }

+ 3 - 0
packages/ui-keyring/src/index.ts

@@ -14,6 +14,7 @@ import isAvailable from './isAvailable';
 import saveAccount from './account/save';
 import saveAccountMeta from './account/meta';
 import forgetAddress from './address/forget';
+import getAccounts from './account/all';
 import getAddress from './address/get';
 import getAddresses from './address/all';
 import saveAddress from './address/meta';
@@ -41,6 +42,8 @@ export default ({
     forgetAddress(state, address),
   isAvailable: (address: string | Uint8Array): boolean =>
     isAvailable(state, address),
+  getAccounts: (): Array<KeyringAddress> =>
+    getAccounts(state),
   getAddress: (address: string | Uint8Array): KeyringAddress =>
     getAddress(state, address),
   getAddresses: (): Array<KeyringAddress> =>

+ 1 - 0
packages/ui-keyring/src/types.d.ts

@@ -60,6 +60,7 @@ export type KeyringInstance = {
   createAccount: (seed: Uint8Array, password?: string, meta?: KeyringPair$Meta) => KeyringPair,
   forgetAccount: (address: string) => void,
   forgetAddress: (address: string) => void,
+  getAccounts: () => Array<KeyringAddress>,
   getAddress: (address: string | Uint8Array) => KeyringAddress,
   getAddresses: () => Array<KeyringAddress>,
   getOptions: (type: KeyringOption$Type) => KeyringOptions,

+ 13 - 0
packages/ui-react-rx/src/with/multi.ts

@@ -0,0 +1,13 @@
+// Copyright 2017-2018 @polkadot/ui-react-rx authors & contributors
+// This software may be modified and distributed under the terms
+// of the ISC license. See the LICENSE file for details.
+
+import React from 'react';
+
+type HOC = (Component: React.ComponentType<any>) => React.ComponentType<any>;
+
+export default function withMulti<T> (Component: React.ComponentType<any>, ...hocs: Array<HOC>): React.ComponentType<any> {
+  return hocs.reduce((Component, hoc) => {
+    return hoc(Component);
+  }, Component);
+}

+ 34 - 3
packages/ui-signer/src/Queue.tsx

@@ -3,13 +3,20 @@
 // of the ISC license. See the LICENSE file for details.
 
 import { BareProps } from '@polkadot/ui-app/types';
-import { QueueProps, QueueTx, QueueTx$Base, QueueTx$Id, QueueTx$Status } from './types';
+import { ApiProps } from '@polkadot/ui-react-rx/types';
+import { QueueProps, QueueTx, QueueTx$Extrinsic, QueueTx$Base, QueueTx$Id, QueueTx$Status } from './types';
 
 import React from 'react';
+import rpcs from '@polkadot/jsonrpc';
+import withApi from '@polkadot/ui-react-rx/with/api';
+import encode from '@polkadot/extrinsics/codec/encode/extrinsic';
+import isUndefined from '@polkadot/util/is/undefined';
 
 import { QueueProvider } from './Context';
 
-export type Props = BareProps & {
+const rpc = rpcs.author.public.submitExtrinsic;
+
+export type Props = BareProps & ApiProps & {
   children: any // node?
 };
 
@@ -21,7 +28,7 @@ const defaultState = {
 
 let nextId: QueueTx$Id = 0;
 
-export default class Queue extends React.Component<Props, State> {
+class Queue extends React.Component<Props, State> {
   state: State = defaultState;
 
   constructor (props: Props) {
@@ -30,6 +37,7 @@ export default class Queue extends React.Component<Props, State> {
     this.state = {
       queue: [],
       queueAdd: this.queueAdd,
+      queueExtrinsic: this.queueExtrinsic,
       queueSetStatus: this.queueSetStatus
     };
   }
@@ -84,4 +92,27 @@ export default class Queue extends React.Component<Props, State> {
 
     return id;
   }
+
+  queueExtrinsic = ({ extrinsic, nonce, publicKey, values }: QueueTx$Extrinsic): QueueTx$Id => {
+    const { apiSupport } = this.props;
+    const params = Object.values(extrinsic.params);
+    const isValid = values.length === params.length &&
+      params.reduce((isValid, param, index) =>
+        isValid && !isUndefined(values[index]),
+        true
+      );
+    const encoded = isValid && extrinsic.params
+      ? encode(extrinsic, values, apiSupport)
+      : new Uint8Array([]);
+
+    return this.queueAdd({
+      isValid,
+      nonce,
+      publicKey,
+      rpc,
+      values: [encoded]
+    });
+  }
 }
+
+export default withApi(Queue);

+ 15 - 2
packages/ui-signer/src/types.d.ts

@@ -3,9 +3,11 @@
 // of the ISC license. See the LICENSE file for details.
 
 import BN from 'bn.js';
+import { Extrinsics } from '@polkadot/extrinsics/types';
 import { SectionItem } from '@polkadot/params/types';
 import { Interfaces } from '@polkadot/jsonrpc/types';
 import { Param$Values } from '@polkadot/params/types';
+import { RawParam$Value } from '@polkadot/ui-app/Params/types';
 
 export type EncodedMessage = {
   isValid: boolean,
@@ -22,12 +24,20 @@ export type QueueTx$Result = {
   status: QueueTx$Status
 }
 
-export type QueueTx$Base = EncodedMessage & {
-  rpc: SectionItem<Interfaces>,
+export type AccountInfo = {
   nonce: BN,
   publicKey?: Uint8Array | null
 };
 
+export type QueueTx$Base = EncodedMessage & AccountInfo & {
+  rpc: SectionItem<Interfaces>
+};
+
+export type QueueTx$Extrinsic = AccountInfo & {
+  extrinsic: SectionItem<Extrinsics>,
+  values: Array<RawParam$Value>
+}
+
 export type QueueTx = QueueTx$Base & {
   error?: Error,
   id: QueueTx$Id,
@@ -37,11 +47,14 @@ export type QueueTx = QueueTx$Base & {
 
 export type QueueTx$MessageAdd = (value: QueueTx$Base) => QueueTx$Id;
 
+export type QueueTx$ExtrinsicAdd = (value: QueueTx$Extrinsic) => QueueTx$Id;
+
 export type QueueTx$MessageSetStatus = (id: number, status: QueueTx$Status, result?: any, error?: Error) => void;
 
 export type QueueProps = {
   queue: Array<QueueTx>,
   queueAdd: QueueTx$MessageAdd,
+  queueExtrinsic: QueueTx$ExtrinsicAdd,
   queueSetStatus: QueueTx$MessageSetStatus
 };
 

+ 1 - 0
tsconfig.json

@@ -9,6 +9,7 @@
       "@polkadot/app-example/*": [ "packages/app-example/src/*" ],
       "@polkadot/app-extrinsics/*": [ "packages/app-extrinsics/src/*" ],
       "@polkadot/app-rpc/*": [ "packages/app-rpc/src/*" ],
+      "@polkadot/app-staking/*": [ "packages/app-staking/src/*" ],
       "@polkadot/app-storage/*": [ "packages/app-storage/src/*" ],
       "@polkadot/app-toolbox/*": [ "packages/app-toolbox/src/*" ],
       "@polkadot/app-vanitygen/*": [ "packages/app-vanitygen/src/*" ],

+ 2 - 2
yarn.lock

@@ -910,7 +910,7 @@
     "@polkadot/trie-hash" "^0.26.35"
     "@polkadot/util" "^0.26.35"
 
-"@polkadot/storage@^0.26.35":
+"@polkadot/storage@^0.26.33", "@polkadot/storage@^0.26.35":
   version "0.26.35"
   resolved "https://registry.yarnpkg.com/@polkadot/storage/-/storage-0.26.35.tgz#2d4e3aac47612b0319c0c69f369974266d1c1d04"
   dependencies:
@@ -945,7 +945,7 @@
     tweetnacl "^1.0.0"
     xxhashjs "^0.2.2"
 
-"@polkadot/util-keyring@^0.26.35":
+"@polkadot/util-keyring@^0.26.33", "@polkadot/util-keyring@^0.26.35":
   version "0.26.35"
   resolved "https://registry.yarnpkg.com/@polkadot/util-keyring/-/util-keyring-0.26.35.tgz#266002c6591665cf8a7df6c5ad3ed840f8b400a5"
   dependencies: