Browse Source

Basic democracy (Referendum) overview (#216)

* Basic democacry (Referendum) overview

* Summary in-place

* Fix package name

* Proposals & Referendums are showing up

* Some basic Extrinsic formatting. (WIP)

* Basic referendum/proposal summary in place

* Referendum outcome chart

* Voting in place. It works.

* Remove extra console.log/error

* Only show balances as label
Jaco Greeff 6 years ago
parent
commit
4484883afc
67 changed files with 1626 additions and 234 deletions
  1. 1 1
      jest.config.js
  2. 15 0
      packages/app-democracy/LICENSE
  3. 1 0
      packages/app-democracy/README.md
  4. 19 0
      packages/app-democracy/package.json
  5. 45 0
      packages/app-democracy/src/Extrinsic.tsx
  6. 64 0
      packages/app-democracy/src/Item.tsx
  7. 86 0
      packages/app-democracy/src/Proposal.tsx
  8. 64 0
      packages/app-democracy/src/Proposals.tsx
  9. 171 0
      packages/app-democracy/src/Referendum.tsx
  10. 64 0
      packages/app-democracy/src/Referendums.tsx
  11. 79 0
      packages/app-democracy/src/Summary.tsx
  12. 72 0
      packages/app-democracy/src/Voting.tsx
  13. 78 0
      packages/app-democracy/src/VotingButtons.tsx
  14. 100 0
      packages/app-democracy/src/index.css
  15. 28 0
      packages/app-democracy/src/index.tsx
  16. 7 0
      packages/app-democracy/src/translate.ts
  17. 1 1
      packages/app-example/package.json
  18. 1 1
      packages/app-explorer/package.json
  19. 1 1
      packages/app-extrinsics/package.json
  20. 2 2
      packages/app-extrinsics/src/Account.tsx
  21. 1 1
      packages/app-extrinsics/src/Params/Account.tsx
  22. 2 2
      packages/app-extrinsics/src/Params/Extrinsic.tsx
  23. 1 1
      packages/app-staking/package.json
  24. 2 2
      packages/app-staking/src/Account.tsx
  25. 2 2
      packages/app-staking/src/StakeList.tsx
  26. 7 6
      packages/app-staking/src/Summary.tsx
  27. 2 2
      packages/app-staking/src/index.tsx
  28. 1 0
      packages/apps/package.json
  29. 1 0
      packages/apps/public/locales/en/democracy.json
  30. 1 0
      packages/apps/public/locales/en/staking.json
  31. 20 0
      packages/apps/src/routing/democracy.ts
  32. 15 1
      packages/apps/src/routing/index.ts
  33. 19 1
      packages/apps/webpack.config.js
  34. 5 7
      packages/ui-app/package.json
  35. 3 2
      packages/ui-app/src/AddressMini.tsx
  36. 3 2
      packages/ui-app/src/Button/index.tsx
  37. 1 0
      packages/ui-app/src/Button/types.d.ts
  38. 1 1
      packages/ui-app/src/CardSummary.tsx
  39. 69 0
      packages/ui-app/src/Chart/Doughnut.tsx
  40. 2 2
      packages/ui-app/src/Params/Param/Account.tsx
  41. 3 3
      packages/ui-app/src/Params/Param/Amount.tsx
  42. 4 3
      packages/ui-app/src/Params/Param/Base.tsx
  43. 7 2
      packages/ui-app/src/Params/Param/BaseBytes.tsx
  44. 2 2
      packages/ui-app/src/Params/Param/Bool.tsx
  45. 8 3
      packages/ui-app/src/Params/Param/Code.tsx
  46. 2 2
      packages/ui-app/src/Params/Param/File.tsx
  47. 1 1
      packages/ui-app/src/Params/Param/Hash.tsx
  48. 2 2
      packages/ui-app/src/Params/Param/StorageKeyValue.tsx
  49. 40 5
      packages/ui-app/src/Params/Param/StorageKeyValueArray.tsx
  50. 4 3
      packages/ui-app/src/Params/Param/String.tsx
  51. 4 3
      packages/ui-app/src/Params/Param/VoteThreshold.tsx
  52. 14 6
      packages/ui-app/src/Params/Param/findComponent.ts
  53. 5 3
      packages/ui-app/src/Params/Param/index.tsx
  54. 27 12
      packages/ui-app/src/Params/index.tsx
  55. 1 1
      packages/ui-app/src/Params/initValue.ts
  56. 3 2
      packages/ui-app/src/Params/types.d.ts
  57. 6 0
      packages/ui-app/src/styles/app.css
  58. 13 0
      packages/ui-app/src/styles/components.css
  59. 3 3
      packages/ui-react-rx/package.json
  60. 241 41
      packages/ui-react-rx/src/ApiObservable/index.ts
  61. 92 0
      packages/ui-react-rx/src/ApiObservable/types.d.ts
  62. 1 49
      packages/ui-react-rx/src/types.d.ts
  63. 2 3
      packages/ui-react-rx/src/with/observable.tsx
  64. 2 1
      packages/ui-react-rx/src/with/observableDiv.ts
  65. 1 1
      packages/ui-signer/package.json
  66. 1 0
      tsconfig.json
  67. 80 45
      yarn.lock

+ 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|staking|storage|toolbox|vanitygen)(.*)$': '<rootDir>/packages/ui-$1/src/$2',
+    '@polkadot/app-(accounts|addresses|democracy|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'

+ 15 - 0
packages/app-democracy/LICENSE

@@ -0,0 +1,15 @@
+ISC License (ISC)
+
+Copyright 2017-2018 @polkadot/app-democracy 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-democracy/README.md

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

+ 19 - 0
packages/app-democracy/package.json

@@ -0,0 +1,19 @@
+{
+  "name": "@polkadot/app-democracy",
+  "version": "0.19.30",
+  "description": "A referendum & proposal 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.28.11",
+    "@polkadot/ui-react": "^0.19.30",
+    "@polkadot/ui-react-rx": "^0.19.30",
+    "@polkadot/util-keyring": "^0.28.1"
+  }
+}

+ 45 - 0
packages/app-democracy/src/Extrinsic.tsx

@@ -0,0 +1,45 @@
+// Copyright 2017-2018 @polkadot/ui-app authors & contributors
+// This software may be modified and distributed under the terms
+// of the ISC license. See the LICENSE file for details.
+
+import { ExtrinsicDecoded } from '@polkadot/params/types';
+import { I18nProps } from '@polkadot/ui-app/types';
+import { RawParam } from '@polkadot/ui-app/Params/types';
+
+import React from 'react';
+import Params from '@polkadot/ui-app/Params';
+import classes from '@polkadot/ui-app/util/classes';
+
+import translate from './translate';
+
+export type Props = I18nProps & {
+  children?: React.ReactNode,
+  value: ExtrinsicDecoded
+};
+
+class Extrinsic extends React.PureComponent<Props> {
+  render () {
+    const { children, className, style, value: { extrinsic, params } } = this.props;
+    const values: Array<RawParam> = extrinsic.params.map(({ type }, index) => ({
+      isValid: true,
+      value: params[index],
+      type
+    }));
+
+    return (
+      <div
+        className={classes('democracy--Extrinsic', className)}
+        style={style}
+      >
+        {children}
+        <Params
+          isDisabled
+          item={extrinsic}
+          values={values}
+        />
+      </div>
+    );
+  }
+}
+
+export default translate(Extrinsic);

+ 64 - 0
packages/app-democracy/src/Item.tsx

@@ -0,0 +1,64 @@
+// Copyright 2017-2018 @polkadot/app-democracy authors & contributors
+// This software may be modified and distributed under the terms
+// of the ISC license. See the LICENSE file for details.
+
+import { ExtrinsicDecoded } from '@polkadot/params/types';
+import { I18nProps } from '@polkadot/ui-app/types';
+
+import BN from 'bn.js';
+import React from 'react';
+import Card from '@polkadot/ui-app/Card';
+import classes from '@polkadot/ui-app/util/classes';
+import numberFormat from '@polkadot/ui-react-rx/util/numberFormat';
+
+// TODO This would be nice as a shared component, move to ui-app as soon as
+// we have actual "more-than-one-use" apps
+import Extrinsic from './Extrinsic';
+import translate from './translate';
+
+type Props = I18nProps & {
+  children?: React.ReactNode,
+  proposal: ExtrinsicDecoded,
+  proposalExtra?: React.ReactNode,
+  idNumber: BN
+};
+
+class Item extends React.PureComponent<Props> {
+  render () {
+    const { children, className, idNumber, proposal, proposalExtra, style } = this.props;
+
+    return (
+      <Card
+        className={classes('democracy--Item', className)}
+        style={style}
+      >
+        <div className='democracy--Item-header'>
+          <div className='democracy--Item-header-info'>
+            <div className='democracy--Item-header-name'>
+              {proposal.extrinsic.section}.{proposal.extrinsic.name}
+            </div>
+            <div className='democracy--Item-header-description'>
+              {proposal.extrinsic.description}
+            </div>
+          </div>
+          <div className='democracy--Item-header-id'>
+            #{numberFormat(idNumber)}
+          </div>
+        </div>
+        <div className='democracy--Item-body'>
+          <Extrinsic
+            className='democracy--Item-extrinsic'
+            value={proposal}
+          >
+            {proposalExtra}
+          </Extrinsic>
+          <div className='democracy--Item-children'>
+            {children}
+          </div>
+        </div>
+      </Card>
+    );
+  }
+}
+
+export default translate(Item);

+ 86 - 0
packages/app-democracy/src/Proposal.tsx

@@ -0,0 +1,86 @@
+// Copyright 2017-2018 @polkadot/app-democracy 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 { RxProposal, RxProposalDeposits } from '@polkadot/ui-react-rx/ApiObservable/types';
+
+import BN from 'bn.js';
+import React from 'react';
+import AddressMini from '@polkadot/ui-app/AddressMini';
+import Labelled from '@polkadot/ui-app/Labelled';
+import Static from '@polkadot/ui-app/Static';
+import classes from '@polkadot/ui-app/util/classes';
+import withObservable from '@polkadot/ui-react-rx/with/observable';
+import withMulti from '@polkadot/ui-react-rx/with/multi';
+import numberFormat from '@polkadot/ui-react-rx/util/numberFormat';
+
+import Item from './Item';
+import translate from './translate';
+
+type Props = I18nProps & {
+  democracyProposalDeposits?: RxProposalDeposits,
+  idNumber: BN,
+  value: RxProposal
+};
+
+class Proposal extends React.PureComponent<Props> {
+  render () {
+    const { className, idNumber, style, value } = this.props;
+
+    return (
+      <Item
+        className={classes('democracy--Proposal', className)}
+        idNumber={idNumber}
+        proposal={value.proposal}
+        proposalExtra={this.renderExtra()}
+        style={style}
+      >
+        {this.renderVoting()}
+      </Item>
+    );
+  }
+
+  private renderExtra () {
+    const { democracyProposalDeposits, t } = this.props;
+
+    if (!democracyProposalDeposits) {
+      return null;
+    }
+
+    const { balance, addresses } = democracyProposalDeposits;
+
+    return (
+      <div className='democracy--Proposal-info'>
+        <Labelled label={t('proposal.depositsAddresses', {
+          defaultValue: 'depositors'
+        })}>
+          <div>
+            {addresses.map((address) => (
+              <AddressMini
+                isPadded={false}
+                key={address}
+                value={address}
+              />
+            ))}
+          </div>
+        </Labelled>
+        <Static label={t('proposal.depositsBalanceLabel', {
+          defaultValue: 'balance'
+        })}>
+          {numberFormat(balance)}
+        </Static>
+      </div>
+    );
+  }
+
+  private renderVoting () {
+    return null;
+  }
+}
+
+export default withMulti(
+  Proposal,
+  translate,
+  withObservable('democracyProposalDeposits', { paramProp: 'idNumber' })
+);

+ 64 - 0
packages/app-democracy/src/Proposals.tsx

@@ -0,0 +1,64 @@
+// Copyright 2017-2018 @polkadot/app-democracy 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 { RxProposal } from '@polkadot/ui-react-rx/ApiObservable/types';
+
+import React from 'react';
+import classes from '@polkadot/ui-app/util/classes';
+import withObservable from '@polkadot/ui-react-rx/with/observable';
+import withMulti from '@polkadot/ui-react-rx/with/multi';
+
+import Proposal from './Proposal';
+import translate from './translate';
+
+type Props = I18nProps & {
+  democracyProposals?: Array<RxProposal>
+};
+
+class Proposals extends React.PureComponent<Props> {
+  render () {
+    const { className, style, t } = this.props;
+
+    return (
+      <div
+        className={classes('democracy--Proposals', className)}
+        style={style}
+      >
+        <h1>{t('proposals.header', {
+          defaultValue: 'proposals'
+        })}</h1>
+        {this.renderProposals()}
+      </div>
+    );
+  }
+
+  private renderProposals () {
+    const { democracyProposals, t } = this.props;
+
+    if (!democracyProposals || !democracyProposals.length) {
+      return (
+        <div className='ui disabled'>
+          {t('proposals.none', {
+            defaultValue: 'no available proposals'
+          })}
+        </div>
+      );
+    }
+
+    return democracyProposals.map((proposal) => (
+      <Proposal
+        idNumber={proposal.id}
+        key={proposal.id.toString()}
+        value={proposal}
+      />
+    ));
+  }
+}
+
+export default withMulti(
+  Proposals,
+  translate,
+  withObservable('democracyProposals')
+);

+ 171 - 0
packages/app-democracy/src/Referendum.tsx

@@ -0,0 +1,171 @@
+// Copyright 2017-2018 @polkadot/app-democracy 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 { RawParam } from '@polkadot/ui-app/Params/types';
+import { RxReferendum, RxReferendumVote } from '@polkadot/ui-react-rx/ApiObservable/types';
+
+import BN from 'bn.js';
+import React from 'react';
+import Static from '@polkadot/ui-app/Static';
+import Doughnut from '@polkadot/ui-app/Chart/Doughnut';
+import VoteThreshold from '@polkadot/ui-app/Params/Param/VoteThreshold';
+import classes from '@polkadot/ui-app/util/classes';
+import numberFormat from '@polkadot/ui-react-rx/util/numberFormat';
+import withObservable from '@polkadot/ui-react-rx/with/observable';
+import withMulti from '@polkadot/ui-react-rx/with/multi';
+
+import Item from './Item';
+import Voting from './Voting';
+import translate from './translate';
+
+const COLORS_YAY = ['#4d4', '#4e4'];
+const COLORS_NAY = ['#d44', '#e44'];
+
+type Props = I18nProps & {
+  bestNumber?: BN,
+  democracyReferendumVoters?: Array<RxReferendumVote>,
+  idNumber: BN,
+  value: RxReferendum
+};
+
+type State = {
+  voteCount: number,
+  voteCountYay: number,
+  voteCountNay: number,
+  votedTotal: BN,
+  votedNay: BN,
+  votedYay: BN
+};
+
+class Referendum extends React.PureComponent<Props, State> {
+  state: State;
+
+  constructor (props: Props) {
+    super(props);
+
+    this.state = {
+      voteCount: 0,
+      voteCountYay: 0,
+      voteCountNay: 0,
+      votedTotal: new BN(0),
+      votedYay: new BN(0),
+      votedNay: new BN(0)
+    };
+  }
+
+  static getDerivedStateFromProps ({ democracyReferendumVoters }: Props, prevState: State): State | null {
+    if (!democracyReferendumVoters) {
+      return null;
+    }
+
+    const newState: State = democracyReferendumVoters.reduce((state, { balance, vote }) => {
+      if (vote) {
+        state.voteCountYay++;
+        state.votedYay = state.votedYay.add(balance);
+      } else {
+        state.voteCountNay++;
+        state.votedNay = state.votedNay.add(balance);
+      }
+
+      state.voteCount++;
+      state.votedTotal = state.votedTotal.add(balance);
+
+      return state;
+    }, {
+      voteCount: 0,
+      voteCountYay: 0,
+      voteCountNay: 0,
+      votedTotal: new BN(0),
+      votedYay: new BN(0),
+      votedNay: new BN(0)
+    });
+
+    if (newState.votedYay.eq(prevState.votedNay) && newState.votedNay.eq(prevState.votedNay)) {
+      return null;
+    }
+
+    return newState;
+  }
+
+  render () {
+    const { className, idNumber, style, value } = this.props;
+
+    return (
+      <Item
+        className={classes('democracy--Referendum', className)}
+        idNumber={idNumber}
+        proposal={value.proposal}
+        proposalExtra={this.renderExtra()}
+        style={style}
+      >
+        <Voting referendumId={idNumber} />
+        {this.renderResults()}
+      </Item>
+    );
+  }
+
+  private renderExtra () {
+    const { bestNumber, t, value: { blockNumber, voteThreshold } } = this.props;
+
+    if (!bestNumber) {
+      return null;
+    }
+
+    return (
+      <div className='democracy--Referendum-info'>
+        <Static label={t('referendum.endLabel', {
+          defaultValue: 'remaining time'
+        })}>
+          {t('referendum.endInfo', {
+            defaultValue: '{{remaining}} blocks remaining, ending at block #{{blockNumber}}',
+            replace: {
+              blockNumber: numberFormat(blockNumber),
+              remaining: numberFormat(blockNumber.sub(bestNumber))
+            }
+          })}
+        </Static>
+        <VoteThreshold
+          isDisabled
+          defaultValue={{ value: voteThreshold } as RawParam}
+          label={t('referendum.thresholdLabel', {
+            defaultValue: 'vote threshold'
+          })}
+          name='voteThreshold'
+        />
+      </div>
+    );
+  }
+
+  private renderResults () {
+    const { voteCount, voteCountYay, voteCountNay, votedTotal, votedYay, votedNay } = this.state;
+
+    if (voteCount === 0 || votedTotal.eqn(0)) {
+      return null;
+    }
+
+    return (
+      <div className='democracy--Referendum-results chart'>
+        <Doughnut values={[
+          {
+            colors: COLORS_YAY,
+            label: `${numberFormat(votedYay)} (${numberFormat(voteCountYay)})`,
+            value: votedYay.muln(10000).div(votedTotal).toNumber() / 100
+          },
+          {
+            colors: COLORS_NAY,
+            label: `${numberFormat(votedNay)} (${numberFormat(voteCountNay)})`,
+            value: votedNay.muln(10000).div(votedTotal).toNumber() / 100
+          }
+        ]} />
+      </div>
+    );
+  }
+}
+
+export default withMulti(
+  translate(Referendum),
+  withObservable('bestNumber'),
+  withObservable('democracyReferendumVoters', { paramProp: 'idNumber' })
+);

+ 64 - 0
packages/app-democracy/src/Referendums.tsx

@@ -0,0 +1,64 @@
+// Copyright 2017-2018 @polkadot/app-democracy 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 { RxReferendum } from '@polkadot/ui-react-rx/ApiObservable/types';
+
+import React from 'react';
+import classes from '@polkadot/ui-app/util/classes';
+import withObservable from '@polkadot/ui-react-rx/with/observable';
+import withMulti from '@polkadot/ui-react-rx/with/multi';
+
+import Referendum from './Referendum';
+import translate from './translate';
+
+type Props = I18nProps & {
+  democracyReferendums?: Array<RxReferendum>
+};
+
+class Referendums extends React.PureComponent<Props> {
+  render () {
+    const { className, style, t } = this.props;
+
+    return (
+      <div
+        className={classes('democracy--Referendums', className)}
+        style={style}
+      >
+        <h1>{t('referendums.header', {
+          defaultValue: 'referendums'
+        })}</h1>
+        {this.renderReferendums()}
+      </div>
+    );
+  }
+
+  private renderReferendums () {
+    const { democracyReferendums, t } = this.props;
+
+    if (!democracyReferendums || !democracyReferendums.length) {
+      return (
+        <div className='ui disabled'>
+          {t('proposals.none', {
+            defaultValue: 'no available referendums'
+          })}
+        </div>
+      );
+    }
+
+    return democracyReferendums.map((referendum) => (
+      <Referendum
+        idNumber={referendum.id}
+        key={referendum.id.toString()}
+        value={referendum}
+      />
+    ));
+  }
+}
+
+export default withMulti(
+  Referendums,
+  translate,
+  withObservable('democracyReferendums')
+);

+ 79 - 0
packages/app-democracy/src/Summary.tsx

@@ -0,0 +1,79 @@
+// Copyright 2017-2018 @polkadot/app-democracy 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 BN from 'bn.js';
+import React from 'react';
+import classes from '@polkadot/ui-app/util/classes';
+import CardSummary from '@polkadot/ui-app/CardSummary';
+import withObservable from '@polkadot/ui-react-rx/with/observable';
+import withMulti from '@polkadot/ui-react-rx/with/multi';
+import numberFormat from '@polkadot/ui-react-rx/util/numberFormat';
+
+import translate from './translate';
+
+type Props = I18nProps & {
+  democracyLaunchPeriod?: BN,
+  democracyNextTally?: BN,
+  democracyProposalCount?: BN,
+  democracyReferendumCount?: BN,
+  democracyVotingPeriod?: BN
+};
+
+class Summary extends React.PureComponent<Props> {
+  render () {
+    const { className, democracyLaunchPeriod, democracyNextTally = new BN(0), democracyProposalCount, democracyReferendumCount = new BN(0), democracyVotingPeriod, style, t } = this.props;
+
+    return (
+      <div
+        className={classes('democracy--Summary', className)}
+        style={style}
+      >
+        <div className='democracy--Summary-column'>
+          <CardSummary label={t('summary.proposalCount', {
+            defaultValue: 'proposals'
+          })}>
+            {numberFormat(democracyProposalCount)}
+          </CardSummary>
+          <CardSummary label={t('summary.referendumCount', {
+            defaultValue: 'referendums'
+          })}>
+            {numberFormat(democracyReferendumCount)}
+          </CardSummary>
+          <CardSummary label={t('summary.active', {
+            defaultValue: 'active num'
+          })}>
+            {democracyNextTally && democracyReferendumCount
+              ? numberFormat(democracyReferendumCount.sub(democracyNextTally))
+              : 0
+            }
+          </CardSummary>
+        </div>
+        <div className='democracy--Summary-column'>
+          <CardSummary label={t('summary.votingPeriod', {
+            defaultValue: 'voting period'
+          })}>
+            {numberFormat(democracyVotingPeriod)}
+          </CardSummary>
+          <CardSummary label={t('summary.launchPeriod', {
+            defaultValue: 'launch period'
+          })}>
+            {numberFormat(democracyLaunchPeriod)}
+          </CardSummary>
+        </div>
+      </div>
+    );
+  }
+}
+
+export default withMulti(
+  Summary,
+  translate,
+  withObservable('democracyLaunchPeriod'),
+  withObservable('democracyReferendumCount'),
+  withObservable('democracyNextTally'),
+  withObservable('democracyProposalCount'),
+  withObservable('democracyVotingPeriod')
+);

+ 72 - 0
packages/app-democracy/src/Voting.tsx

@@ -0,0 +1,72 @@
+// Copyright 2017-2018 @polkadot/app-democracy 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 BN from 'bn.js';
+import React from 'react';
+import InputAddress from '@polkadot/ui-app/InputAddress';
+import { QueueConsumer } from '@polkadot/ui-signer/Context';
+
+import VotingButtons from './VotingButtons';
+import translate from './translate';
+
+type Props = I18nProps & {
+  referendumId: BN
+};
+
+type State = {
+  publicKey?: Uint8Array
+};
+
+class Voting extends React.PureComponent<Props, State> {
+  state: State = {};
+
+  render () {
+    const { t } = this.props;
+
+    return (
+      <div className='democracy--Referendum-vote'>
+        <InputAddress
+          label={t('voting.account', {
+            defaultValue: 'vote using my account'
+          })}
+          onChange={this.onChangeAccount}
+          placeholder='0x...'
+          type='account'
+          withLabel
+        />
+        {this.renderButtons()}
+      </div>
+    );
+  }
+
+  private renderButtons () {
+    const { referendumId } = this.props;
+    const { publicKey } = this.state;
+
+    if (!publicKey) {
+      return null;
+    }
+
+    return (
+      <QueueConsumer>
+        {({ queueExtrinsic }: QueueProps) => (
+          <VotingButtons
+            publicKey={publicKey}
+            queueExtrinsic={queueExtrinsic}
+            referendumId={referendumId}
+          />
+        )}
+      </QueueConsumer>
+    );
+  }
+
+  private onChangeAccount = (publicKey?: Uint8Array) => {
+    this.setState({ publicKey });
+  }
+}
+
+export default translate(Voting);

+ 78 - 0
packages/app-democracy/src/VotingButtons.tsx

@@ -0,0 +1,78 @@
+// Copyright 2017-2018 @polkadot/app-democracy 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 { 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 withMulti from '@polkadot/ui-react-rx/with/multi';
+import withObservable from '@polkadot/ui-react-rx/with/observable';
+
+import translate from './translate';
+
+type Props = I18nProps & {
+  publicKey?: Uint8Array,
+  queueExtrinsic: QueueTx$ExtrinsicAdd,
+  referendumId: BN,
+  systemAccountIndexOf?: BN
+};
+
+class VotingButton extends React.PureComponent<Props> {
+  render () {
+    const { publicKey, t } = this.props;
+
+    return (
+      <Button.Group>
+        <Button
+          isDisabled={!publicKey}
+          isPositive
+          text={t('votebtn.yay', {
+            defaultValue: 'yay'
+          })}
+          onClick={this.onClickYay}
+        />
+        <Button.Or />
+        <Button
+          isDisabled={!publicKey}
+          isNegative
+          text={t('votebtn.nay', {
+            defaultValue: 'nay'
+          })}
+          onClick={this.onClickNay}
+        />
+      </Button.Group>
+    );
+  }
+
+  private doVote (vote: boolean) {
+    const { publicKey, queueExtrinsic, referendumId, systemAccountIndexOf = new BN(0) } = this.props;
+
+    if (!publicKey) {
+      return;
+    }
+
+    queueExtrinsic({
+      extrinsic: extrinsics.democracy.public.vote,
+      nonce: systemAccountIndexOf,
+      publicKey,
+      values: [referendumId, vote]
+    });
+  }
+
+  private onClickYay = () => {
+    this.doVote(true);
+  }
+
+  private onClickNay = () => {
+    this.doVote(false);
+  }
+}
+
+export default withMulti(
+  translate(VotingButton),
+  withObservable('systemAccountIndexOf', { paramProp: 'publicKey' })
+);

+ 100 - 0
packages/app-democracy/src/index.css

@@ -0,0 +1,100 @@
+/* Copyright 2017-2018 @polkadot/app-democracy authors & contributors
+/* This software may be modified and distributed under the terms
+/* of the ISC license. See the LICENSE file for details. */
+
+.democracy--App {
+  margin-bottom: 1em;
+}
+
+/* FIXME These are just copied as-is from staking/Summary - once happy and proven to be
+  shareable, it needs to be moved into ui-app for consistency.
+*/
+.democracy--Summary {
+  align-items: stretch;
+  display: flex;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  margin-bottom: 2em;
+}
+
+.democracy--Summary-column {
+  display: flex;
+  flex: 0 1 auto;
+  text-align: left;
+}
+
+.democracy--Summary-card {
+  flex: 0 1 auto;
+  text-align: left;
+}
+
+.democracy--Proposals,
+.democracy--Referendums {
+  margin-bottom: 2rem;
+}
+
+.democracy--Item-header {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 1rem;
+}
+
+.democracy--Item-header-info,
+.democracy--Item-header-id {
+  flex: 1;
+}
+
+.democracy--Item-header-id {
+  font-size: 2rem;
+  line-height: 2rem;
+  text-align: right;
+}
+
+.democracy--Item-header-description {
+  color: rgba(0, 0, 0, 0.6);
+}
+
+.democracy--Item-header-name {
+  font-size: 1.2rem;
+  line-height: 1.2rem;
+}
+
+.democracy--Item-body {
+  display: flex;
+  flex-direction: row;
+  justify-content: stretch;
+}
+
+.democracy--Item-extrinsic,
+.democracy--Item-children {
+  flex: 1;
+  width: 0;
+  padding: 0 1em;
+  text-align: left;
+}
+
+.democracy--Item-extrinsic .ui--Params-Content {
+  padding-left: 0;
+}
+
+.democracy--Extrinsic .ui--Labelled {
+  margin-bottom: 0.25rem;
+}
+
+.democracy--Extrinsic .ui--Labelled .ui.label {
+  display: block;
+  padding-left: 0;
+}
+
+.democracy--Proposal-info,
+.democracy--Referendum-info {
+  margin-bottom: 0;
+}
+
+.democracy--Referendum-results {
+  margin-bottom: 1em;
+}
+
+.democracy--Referendum-results.chart {
+  text-align: center;
+}

+ 28 - 0
packages/app-democracy/src/index.tsx

@@ -0,0 +1,28 @@
+// Copyright 2017-2018 @polkadot/app-democracy 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 './index.css';
+
+import React from 'react';
+import Page from '@polkadot/ui-app/Page';
+
+import Proposals from './Proposals';
+import Referendums from './Referendums';
+import Summary from './Summary';
+
+type Props = BareProps & {};
+
+export default class App extends React.PureComponent<Props> {
+  render () {
+    return (
+      <Page className='democracy--App'>
+        <Summary />
+        <Referendums />
+        <Proposals />
+      </Page>
+    );
+  }
+}

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

@@ -0,0 +1,7 @@
+// Copyright 2017-2018 @polkadot/app-explorer 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(['democracy', 'ui']);

+ 1 - 1
packages/app-example/package.json

@@ -11,7 +11,7 @@
   "license": "ISC",
   "dependencies": {
     "@babel/runtime": "^7.0.0-beta.51",
-    "@polkadot/storage": "^0.28.9",
+    "@polkadot/storage": "^0.28.11",
     "@polkadot/ui-react": "^0.19.30",
     "@polkadot/ui-react-rx": "^0.19.30",
     "@polkadot/util-keyring": "^0.28.1"

+ 1 - 1
packages/app-explorer/package.json

@@ -16,7 +16,7 @@
   },
   "dependencies": {
     "@babel/runtime": "^7.0.0-beta.51",
-    "@polkadot/primitives": "^0.28.9",
+    "@polkadot/primitives": "^0.28.11",
     "@polkadot/ui-app": "^0.19.30",
     "@polkadot/util-crypto": "^0.28.1"
   }

+ 1 - 1
packages/app-extrinsics/package.json

@@ -16,7 +16,7 @@
   },
   "dependencies": {
     "@babel/runtime": "^7.0.0-beta.51",
-    "@polkadot/extrinsics": "^0.28.9",
+    "@polkadot/extrinsics": "^0.28.11",
     "@polkadot/ui-app": "^0.19.30",
     "@polkadot/ui-signer": "^0.19.30",
     "react-dropzone": "^4.2.9"

+ 2 - 2
packages/app-extrinsics/src/Account.tsx

@@ -20,7 +20,7 @@ type Props = I18nProps & {
   isError?: boolean,
   isInput?: boolean,
   label: string,
-  onChange: (publicKey: Uint8Array) => void,
+  onChange?: (publicKey: Uint8Array) => void,
   type?: KeyringOption$Type,
   withLabel?: boolean
 };
@@ -94,7 +94,7 @@ class Account extends React.PureComponent<Props, State> {
     const { onChange } = this.props;
 
     this.setState({ publicKey }, () =>
-      onChange(publicKey)
+      onChange && onChange(publicKey)
     );
   }
 }

+ 1 - 1
packages/app-extrinsics/src/Params/Account.tsx

@@ -31,7 +31,7 @@ export default class Account extends React.PureComponent<Props> {
   onChange = (publicKey?: Uint8Array): void => {
     const { onChange } = this.props;
 
-    onChange({
+    onChange && onChange({
       isValid: !!publicKey && publicKey.length === 32,
       value: publicKey
     });

+ 2 - 2
packages/app-extrinsics/src/Params/Extrinsic.tsx

@@ -19,7 +19,7 @@ type Props = I18nProps & {
   isError?: boolean,
   isPrivate: boolean,
   label: string,
-  onChange: RawParam$OnChange,
+  onChange?: RawParam$OnChange,
   withLabel?: boolean
 };
 
@@ -56,7 +56,7 @@ class Extrinsic extends React.PureComponent<Props> {
   onChange = ({ isValid, values }: EncodedMessage): void => {
     const { onChange } = this.props;
 
-    onChange({
+    onChange && onChange({
       isValid,
       value: (values[0] as Uint8Array)
     });

+ 1 - 1
packages/app-staking/package.json

@@ -12,7 +12,7 @@
   "dependencies": {
     "@babel/runtime": "^7.0.0-beta.51",
     "@polkadot/app-extrinsics": "^0.19.30",
-    "@polkadot/storage": "^0.28.9",
+    "@polkadot/storage": "^0.28.11",
     "@polkadot/ui-app": "^0.19.30",
     "@polkadot/ui-keyring": "^0.19.30",
     "@polkadot/ui-react": "^0.19.30",

+ 2 - 2
packages/app-staking/src/Account.tsx

@@ -7,7 +7,7 @@ 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 { ExtendedBalanceMap } from '@polkadot/ui-react-rx/types';
+import { RxBalanceMap } from '@polkadot/ui-react-rx/ApiObservable/types';
 
 import BN from 'bn.js';
 import React from 'react';
@@ -29,7 +29,7 @@ import translate from './translate';
 type Props = I18nProps & {
   systemAccountIndexOf?: BN,
   address: string,
-  balances: ExtendedBalanceMap,
+  balances: RxBalanceMap,
   name: string,
   stakingNominating?: string,
   stakingNominatorsFor?: Array<string>,

+ 2 - 2
packages/app-staking/src/StakeList.tsx

@@ -4,7 +4,7 @@
 
 import { I18nProps } from '@polkadot/ui-app/types';
 import { QueueProps } from '@polkadot/ui-signer/types';
-import { ExtendedBalanceMap } from '@polkadot/ui-react-rx/types';
+import { RxBalanceMap } from '@polkadot/ui-react-rx/ApiObservable/types';
 
 import React from 'react';
 import classes from '@polkadot/ui-app/util/classes';
@@ -15,7 +15,7 @@ import Account from './Account';
 import translate from './translate';
 
 type Props = I18nProps & {
-  balances: ExtendedBalanceMap,
+  balances: RxBalanceMap,
   intentions: Array<string>,
   validators: Array<string>
 };

+ 7 - 6
packages/app-staking/src/Summary.tsx

@@ -3,7 +3,8 @@
 // of the ISC license. See the LICENSE file for details.
 
 import { I18nProps } from '@polkadot/ui-app/types';
-import { ApiProps, ExtendedBalance, ExtendedBalanceMap } from '@polkadot/ui-react-rx/types';
+import { RxBalance, RxBalanceMap } from '@polkadot/ui-react-rx/ApiObservable/types';
+import { ApiProps } from '@polkadot/ui-react-rx/types';
 
 import BN from 'bn.js';
 import React from 'react';
@@ -15,7 +16,7 @@ import numberFormat from '@polkadot/ui-react-rx/util/numberFormat';
 import translate from './translate';
 
 type Props = ApiProps & I18nProps & {
-  balances: ExtendedBalanceMap,
+  balances: RxBalanceMap,
   bestNumber?: BN,
   intentions: Array<string>,
   lastLengthChange?: BN,
@@ -74,10 +75,10 @@ class Summary extends React.PureComponent<Props> {
     );
   }
 
-  private calcIntentionsHigh (): ExtendedBalance | null {
+  private calcIntentionsHigh (): RxBalance | null {
     const { balances, intentions, validators } = this.props;
 
-    return intentions.reduce((high: ExtendedBalance | null, addr) => {
+    return intentions.reduce((high: RxBalance | null, addr) => {
       const balance = validators.includes(addr) || !balances[addr]
         ? null
         : balances[addr];
@@ -90,10 +91,10 @@ class Summary extends React.PureComponent<Props> {
     }, null);
   }
 
-  private calcValidatorLow (): ExtendedBalance | null {
+  private calcValidatorLow (): RxBalance | null {
     const { balances, validators } = this.props;
 
-    return validators.reduce((low: ExtendedBalance | null, addr) => {
+    return validators.reduce((low: RxBalance | null, addr) => {
       const balance = balances[addr] || null;
 
       if (low === null || (balance && low.stakingBalance.gt(balance.stakingBalance))) {

+ 2 - 2
packages/app-staking/src/index.tsx

@@ -3,7 +3,7 @@
 // of the ISC license. See the LICENSE file for details.
 
 import { BareProps } from '@polkadot/ui-app/types';
-import { ExtendedBalanceMap } from '@polkadot/ui-react-rx/types';
+import { RxBalanceMap } from '@polkadot/ui-react-rx/ApiObservable/types';
 
 import React from 'react';
 import Page from '@polkadot/ui-app/Page';
@@ -17,7 +17,7 @@ import StakeList from './StakeList';
 import Summary from './Summary';
 
 type Props = BareProps & {
-  validatingBalances?: ExtendedBalanceMap,
+  validatingBalances?: RxBalanceMap,
   stakingIntentions?: Array<string>,
   sessionValidators?: Array<string>
 };

+ 1 - 0
packages/apps/package.json

@@ -19,6 +19,7 @@
     "@babel/runtime": "^7.0.0-beta.51",
     "@polkadot/app-accounts": "^0.19.30",
     "@polkadot/app-addresses": "^0.19.30",
+    "@polkadot/app-democracy": "^0.19.30",
     "@polkadot/app-example": "^0.19.30",
     "@polkadot/app-explorer": "^0.19.30",
     "@polkadot/app-extrinsics": "^0.19.30",

+ 1 - 0
packages/apps/public/locales/en/democracy.json

@@ -0,0 +1 @@
+{}

+ 1 - 0
packages/apps/public/locales/en/staking.json

@@ -0,0 +1 @@
+{}

+ 20 - 0
packages/apps/src/routing/democracy.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 Democracy from '@polkadot/app-democracy/index';
+
+export default ([
+  {
+    Component: Democracy,
+    i18n: {
+      defaultValue: 'Democracy'
+    },
+    icon: 'calendar check',
+    isExact: false,
+    isHidden: false,
+    name: 'democracy'
+  }
+] as Routes);

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

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

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

@@ -10,7 +10,25 @@ 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-staking', '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-democracy',
+  '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'));

+ 5 - 7
packages/ui-app/package.json

@@ -9,28 +9,26 @@
   ],
   "contributors": [],
   "license": "ISC",
-  "scripts": {
-    "build": "polkadot-dev-build-babel",
-    "check": "stylelint 'src/**/*.css' && eslint src && flow check",
-    "test": "jest --coverage"
-  },
   "dependencies": {
     "@babel/runtime": "^7.0.0-beta.51",
-    "@polkadot/extrinsics": "^0.28.9",
-    "@polkadot/storage": "^0.28.9",
+    "@polkadot/extrinsics": "^0.28.11",
+    "@polkadot/storage": "^0.28.11",
     "@polkadot/ui-keyring": "^0.19.30",
     "@polkadot/ui-react": "^0.19.30",
     "@polkadot/ui-react-rx": "^0.19.30",
+    "@types/chart.js": "^2.7.30",
     "@types/i18next-browser-languagedetector": "^2.0.1",
     "@types/i18next-xhr-backend": "^1.4.1",
     "@types/react-copy-to-clipboard": "^4.2.5",
     "@types/react-dropzone": "^4.2.0",
     "@types/react-i18next": "^7.6.1",
     "@types/react-router-dom": "^4.2.7",
+    "chart.js": "^2.7.2",
     "i18next": "^11.1.1",
     "i18next-browser-languagedetector": "^2.2.0",
     "i18next-xhr-backend": "^1.5.1",
     "react": "^16.4.1",
+    "react-chartjs-2": "^2.7.4",
     "react-copy-to-clipboard": "^5.0.1",
     "react-dom": "^16.4.1",
     "react-i18next": "^7.7.0",

+ 3 - 2
packages/ui-app/src/AddressMini.tsx

@@ -14,6 +14,7 @@ import Balance from './Balance';
 
 type Props = BareProps & {
   balance?: BN | Array<BN>,
+  isPadded?: boolean,
   isShort?: boolean,
   value?: string,
   withBalance?: boolean
@@ -21,7 +22,7 @@ type Props = BareProps & {
 
 export default class AddressMini extends React.PureComponent<Props> {
   render () {
-    const { className, isShort = true, style, value } = this.props;
+    const { className, isPadded = true, isShort = true, style, value } = this.props;
 
     if (!value) {
       return null;
@@ -29,7 +30,7 @@ export default class AddressMini extends React.PureComponent<Props> {
 
     return (
       <div
-        className={classes('ui--AddressMini', className)}
+        className={classes('ui--AddressMini', isPadded ? 'padded' : '', className)}
         style={style}
       >
         <div className='ui--AddressMini-info'>

+ 3 - 2
packages/ui-app/src/Button/index.tsx

@@ -17,7 +17,7 @@ import Or from './Or';
 
 class Button extends React.PureComponent<ButtonProps> {
   render () {
-    const { children, className, floated, icon, isBasic = false, isCircular = false, isDisabled = false, isNegative = false, isPrimary = false, onClick, size, style, text } = this.props;
+    const { children, className, floated, icon, isBasic = false, isCircular = false, isDisabled = false, isNegative = false, isPositive = false, isPrimary = false, onClick, size, style, text } = this.props;
 
     const props = {
       basic: isBasic || false,
@@ -28,9 +28,10 @@ class Button extends React.PureComponent<ButtonProps> {
       icon,
       negative: isNegative,
       onClick,
+      positive: isPositive,
       primary: isPrimary,
       size,
-      secondary: isBasic && !(isPrimary || isNegative || false),
+      secondary: isBasic && !(isPositive || isPrimary || isNegative || false),
       style
     };
 

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

@@ -14,6 +14,7 @@ export type ButtonProps = BareProps & {
   isCircular?: boolean,
   isDisabled?: boolean,
   isNegative?: boolean,
+  isPositive?: boolean,
   isPrimary?: boolean,
   onClick?: () => void | Promise<void>,
   size?: Button$Sizes,

+ 1 - 1
packages/ui-app/src/CardSummary.tsx

@@ -38,7 +38,7 @@ export default class CardSummary extends React.PureComponent<Props> {
           <div className='ui--CardSummary-large'>
             {children}{
               progress && (
-                (isUndefined(progress.value) || progress.value.ltn(0)) || isUndefined(progress.total)
+                (isUndefined(progress.value) || isUndefined(progress.total) || progress.value.ltn(0)) || progress.value.gt(progress.total) || progress.total.eqn(0)
                   ? '-'
                   : `${progress.value.toString()}/${progress.total.toString()}`
               )

+ 69 - 0
packages/ui-app/src/Chart/Doughnut.tsx

@@ -0,0 +1,69 @@
+// Copyright 2017-2018 @polkadot/ui-app 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 '../types';
+
+import BN from 'bn.js';
+import React from 'react';
+import { Doughnut } from 'react-chartjs-2';
+import bnToBn from '@polkadot/util/bn/toBn';
+
+import classes from '../util/classes';
+
+type Value = {
+  colors: Array<string>,
+  label: string,
+  value: number | BN
+};
+
+type Props = BareProps & {
+  size?: number,
+  values: Array<Value>
+};
+
+type Options = {
+  colorNormal: Array<string>,
+  colorHover: Array<string>,
+  data: Array<number>,
+  labels: Array<string>
+};
+
+export default class ChartDoughnut extends React.PureComponent<Props> {
+  render () {
+    const { className, size = 100, style, values } = this.props;
+
+    const options = values.reduce((options, { colors: [normalColor = '#00f', hoverColor], label, value }) => {
+      options.colorNormal.push(normalColor);
+      options.colorHover.push(hoverColor || normalColor);
+      options.data.push(bnToBn(value).toNumber());
+      options.labels.push(label);
+
+      return options;
+    }, {
+      colorNormal: [],
+      colorHover: [],
+      data: [],
+      labels: []
+    } as Options);
+
+    return (
+      <div
+        className={classes('ui--Chart', className)}
+        style={style}
+      >
+        <Doughnut
+          data={{
+            labels: options.labels,
+            datasets: [{
+              data: options.data,
+              backgroundColor: options.colorNormal,
+              hoverBackgroundColor: options.colorHover
+            }]
+          }}
+          height={size}
+          width={size} />
+      </div>
+    );
+  }
+}

+ 2 - 2
packages/ui-app/src/Params/Param/Account.tsx

@@ -20,7 +20,7 @@ export default class Account extends React.PureComponent<Props> {
         style={style}
       >
         <InputAddress
-          className='large'
+          className={isDisabled ? 'full' : 'large'}
           defaultValue={defaultValue}
           isDisabled={isDisabled}
           isError={isError}
@@ -37,7 +37,7 @@ export default class Account extends React.PureComponent<Props> {
   onChange = (value?: Uint8Array): void => {
     const { onChange } = this.props;
 
-    onChange({
+    onChange && onChange({
       isValid: !!value && value.length === 32,
       value
     });

+ 3 - 3
packages/ui-app/src/Params/Param/Amount.tsx

@@ -21,8 +21,8 @@ export default class Amount extends React.PureComponent<Props> {
         style={style}
       >
         <Input
-          className='small'
-          defaultValue={defaultValue || 0}
+          className={isDisabled ? 'full' : 'small'}
+          defaultValue={defaultValue}
           isDisabled={isDisabled}
           isError={isError}
           label={label}
@@ -38,7 +38,7 @@ export default class Amount extends React.PureComponent<Props> {
   onChange = (value: string): void => {
     const { onChange } = this.props;
 
-    onChange({
+    onChange && onChange({
       isValid: true,
       value: new BN(value || 0)
     });

+ 4 - 3
packages/ui-app/src/Params/Param/Base.tsx

@@ -11,20 +11,21 @@ import Labelled from '../../Labelled';
 import Bare from './Bare';
 
 type Props = BareProps & {
-  children: any, // node?
+  children: React.ReactNode,
+  isDisabled?: boolean,
   label?: string,
   size?: Size,
   withLabel?: boolean
 };
 
-export default function Base ({ children, className, label, size = 'medium', style, withLabel }: Props) {
+export default function Base ({ children, className, isDisabled, label, size = 'medium', style, withLabel }: Props) {
   return (
     <Bare
       className={className}
       style={style}
     >
       <Labelled
-        className={size}
+        className={isDisabled ? 'full' : size}
         label={label}
         withLabel={withLabel}
       >

+ 7 - 2
packages/ui-app/src/Params/Param/BaseBytes.tsx

@@ -8,6 +8,7 @@ import React from 'react';
 import hexToU8a from '@polkadot/util/hex/toU8a';
 import bnToU8a from '@polkadot/util/bn/toU8a';
 import u8aConcat from '@polkadot/util/u8a/concat';
+import u8aToHex from '@polkadot/util/u8a/toHex';
 
 import Input from '../../Input';
 import Bare from './Bare';
@@ -24,7 +25,10 @@ const defaultValidate = (u8a: Uint8Array): boolean =>
 
 export default class BaseBytes extends React.PureComponent<Props> {
   render () {
-    const { className, isDisabled, isError, label, size = 'full', style, withLabel } = this.props;
+    const { className, defaultValue: { value }, isDisabled, isError, label, size = 'full', style, withLabel } = this.props;
+    const defaultValue = value
+      ? u8aToHex(value as Uint8Array)
+      : undefined;
 
     return (
       <Bare
@@ -33,6 +37,7 @@ export default class BaseBytes extends React.PureComponent<Props> {
       >
         <Input
           className={size}
+          defaultValue={defaultValue}
           isDisabled={isDisabled}
           isError={isError}
           label={label}
@@ -61,7 +66,7 @@ export default class BaseBytes extends React.PureComponent<Props> {
       : u8a.length !== 0;
     const isValid = isValidLength && validate(u8a);
 
-    onChange({
+    onChange && onChange({
       isValid,
       value: u8aConcat(
         withLength

+ 2 - 2
packages/ui-app/src/Params/Param/Bool.tsx

@@ -25,7 +25,7 @@ export default class Bool extends React.PureComponent<Props> {
         style={style}
       >
         <Dropdown
-          className='small'
+          className={isDisabled ? 'full' : 'small'}
           defaultValue={defaultValue}
           isDisabled={isDisabled}
           isError={isError}
@@ -41,7 +41,7 @@ export default class Bool extends React.PureComponent<Props> {
   onChange = (value: boolean): void => {
     const { onChange } = this.props;
 
-    onChange({
+    onChange && onChange({
       isValid: true,
       value
     });

+ 8 - 3
packages/ui-app/src/Params/Param/Code.tsx

@@ -9,15 +9,20 @@ import React from 'react';
 import bnToU8a from '@polkadot/util/bn/toU8a';
 import u8aConcat from '@polkadot/util/u8a/concat';
 
+import Bytes from './Bytes';
 import BytesFile from './File';
 
 export default class Code extends React.PureComponent<Props> {
   render () {
-    const { className, isDisabled, isError, label, style, withLabel } = this.props;
+    const { className, defaultValue, isDisabled, isError, label, style, withLabel } = this.props;
+    const Component = isDisabled
+      ? Bytes
+      : BytesFile;
 
     return (
-      <BytesFile
+      <Component
         className={className}
+        defaultValue={defaultValue}
         isDisabled={isDisabled}
         isError={isError}
         label={label}
@@ -32,7 +37,7 @@ export default class Code extends React.PureComponent<Props> {
   onChange = (value: Uint8Array): void => {
     const { onChange } = this.props;
 
-    onChange({
+    onChange && onChange({
       isValid: value.length !== 0,
       value: u8aConcat(bnToU8a(value.length, 32, true), value)
     });

+ 2 - 2
packages/ui-app/src/Params/Param/File.tsx

@@ -16,7 +16,7 @@ type Props = BareProps & {
   isDisabled?: boolean,
   isError?: boolean,
   label: string,
-  onChange: (contents: Uint8Array) => void,
+  onChange?: (contents: Uint8Array) => void,
   placeholder?: string,
   t: TranslationFunction,
   withLabel?: boolean
@@ -87,7 +87,7 @@ class BytesFile extends React.PureComponent<Props, State> {
       reader.onload = ({ target: { result } }: LoadEvent) => {
         const data = new Uint8Array(result);
 
-        onChange(data);
+        onChange && onChange(data);
 
         this.setState({
           file: {

+ 1 - 1
packages/ui-app/src/Params/Param/Hash.tsx

@@ -19,7 +19,7 @@ export default function Hash ({ className, defaultValue, isDisabled, isError, la
       length={32}
       name={name}
       onChange={onChange}
-      size='medium'
+      size={isDisabled ? 'full' : 'medium'}
       style={style}
       withLabel={withLabel}
     />

+ 2 - 2
packages/ui-app/src/Params/Param/KeyValue.tsx → packages/ui-app/src/Params/Param/StorageKeyValue.tsx

@@ -23,7 +23,7 @@ type State = {
   value: State$Param
 };
 
-export default class KeyValue extends React.PureComponent<Props, State> {
+export default class StorageKeyValue extends React.PureComponent<Props, State> {
   state: State = {
     key: {
       isValid: false,
@@ -91,7 +91,7 @@ export default class KeyValue extends React.PureComponent<Props, State> {
       (prevState: State, { onChange }: Props) => {
         const { key = prevState.key, value = prevState.value } = newState;
 
-        onChange({
+        onChange && onChange({
           isValid: key.isValid && value.isValid,
           value: u8aConcat(
             u8aConcat(bnToU8a(key.u8a.length, 32, true), key.u8a),

+ 40 - 5
packages/ui-app/src/Params/Param/KeyValueStorageArray.tsx → packages/ui-app/src/Params/Param/StorageKeyValueArray.tsx

@@ -4,15 +4,18 @@
 
 import { TranslationFunction } from 'i18next';
 import { KeyValue } from '@polkadot/params/types';
-import { Props as BaseProps } from '../types';
+import { Props as BaseProps, RawParam } from '../types';
 
 import React from 'react';
 import assert from '@polkadot/util/assert';
 import isHex from '@polkadot/util/is/hex';
 import u8aToUtf8 from '@polkadot/util/u8a/toUtf8';
 import toU8a from '@polkadot/util/u8a/toU8a';
+import u8aToHex from '@polkadot/util/u8a/toHex';
 
 import translate from '../../translate';
+import Base from './Base';
+import Bytes from './Bytes';
 import BytesFile from './File';
 
 type Props = BaseProps & {
@@ -23,7 +26,9 @@ type State = {
   placeholder?: string;
 };
 
-class KeyValueStorageArray extends React.PureComponent<Props, State> {
+type Pairs = Array<KeyValue>;
+
+class StorageKeyValueArray extends React.PureComponent<Props, State> {
   private placeholderEmpty: string;
 
   constructor (props: Props) {
@@ -41,6 +46,10 @@ class KeyValueStorageArray extends React.PureComponent<Props, State> {
     const { className, isDisabled, isError, label, style, withLabel } = this.props;
     const { placeholder } = this.state;
 
+    if (isDisabled) {
+      return this.renderReadOnly();
+    }
+
     return (
       <BytesFile
         className={className}
@@ -55,7 +64,33 @@ class KeyValueStorageArray extends React.PureComponent<Props, State> {
     );
   }
 
-  onChange = (raw: Uint8Array): void => {
+  private renderReadOnly () {
+    const { className, defaultValue: { value }, label, style } = this.props;
+
+    return (
+      <Base
+        className={className}
+        label={label}
+        size='full'
+        style={style}
+      >
+        {(value as Pairs).map(({ key, value }) => {
+          const keyHex = u8aToHex(key);
+
+          return (
+            <Bytes
+              defaultValue={{ value } as RawParam}
+              key={keyHex}
+              label={keyHex}
+              name={keyHex}
+            />
+          );
+        })}
+      </Base>
+    );
+  }
+
+  private onChange = (raw: Uint8Array): void => {
     const { onChange, t } = this.props;
     let value: KeyValue[] = [];
 
@@ -78,7 +113,7 @@ class KeyValueStorageArray extends React.PureComponent<Props, State> {
       });
     }
 
-    onChange({
+    onChange && onChange({
       isValid: value.length !== 0,
       value
     });
@@ -100,4 +135,4 @@ class KeyValueStorageArray extends React.PureComponent<Props, State> {
   }
 }
 
-export default translate(KeyValueStorageArray);
+export default translate(StorageKeyValueArray);

+ 4 - 3
packages/ui-app/src/Params/Param/String.tsx

@@ -11,7 +11,8 @@ import Bare from './Bare';
 
 export default class StringParam extends React.PureComponent<Props> {
   render () {
-    const { className, isDisabled, isError, label, style, withLabel } = this.props;
+    const { className, defaultValue: { value }, isDisabled, isError, label, style, withLabel } = this.props;
+    const defaultValue = value as string;
 
     return (
       <Bare
@@ -20,6 +21,7 @@ export default class StringParam extends React.PureComponent<Props> {
       >
         <Input
           className='full'
+          defaultValue={defaultValue}
           isDisabled={isDisabled}
           isError={isError}
           label={label}
@@ -34,10 +36,9 @@ export default class StringParam extends React.PureComponent<Props> {
 
   onChange = (value: string): void => {
     const { onChange } = this.props;
-
     const isValid = value.length !== 0;
 
-    onChange({
+    onChange && onChange({
       isValid,
       value
     });

+ 4 - 3
packages/ui-app/src/Params/Param/VoteThreshold.tsx

@@ -5,6 +5,7 @@
 import { Props } from '../types';
 
 import React from 'react';
+import bnToBn from '@polkadot/util/bn/toBn';
 
 import Dropdown from '../../Dropdown';
 import Bare from './Bare';
@@ -26,7 +27,7 @@ export const textMap = options.reduce((textMap, { text, value }) => {
 export default class VoteThreshold extends React.PureComponent<Props> {
   render () {
     const { className, defaultValue: { value }, isDisabled, isError, label, style, withLabel } = this.props;
-    const defaultValue = (value as number);
+    const defaultValue = bnToBn(value as number).toNumber();
 
     return (
       <Bare
@@ -34,7 +35,7 @@ export default class VoteThreshold extends React.PureComponent<Props> {
         style={style}
       >
         <Dropdown
-          className='small'
+          className={isDisabled ? 'full' : 'small'}
           defaultValue={defaultValue}
           isDisabled={isDisabled}
           isError={isError}
@@ -50,7 +51,7 @@ export default class VoteThreshold extends React.PureComponent<Props> {
   onChange = (value: number): void => {
     const { onChange } = this.props;
 
-    onChange({
+    onChange && onChange({
       isValid: true,
       value
     });

+ 14 - 6
packages/ui-app/src/Params/Param/findComponent.ts

@@ -11,8 +11,8 @@ import Bool from './Bool';
 import Bytes from './Bytes';
 import Code from './Code';
 import Hash from './Hash';
-import KeyValue from './KeyValue';
-import KeyValueStorageArray from './KeyValueStorageArray';
+import StorageKeyValue from './StorageKeyValue';
+import StorageKeyValueArray from './StorageKeyValueArray';
 import StringParam from './String';
 import Unknown from './Unknown';
 import VoteThreshold from './VoteThreshold';
@@ -28,9 +28,9 @@ const components: ComponentMap = {
   'Digest': Unknown,
   'Hash': Hash,
   'Index': Amount,
-  'KeyValue': KeyValue,
-  'KeyValueStorage': KeyValue,
-  'KeyValueStorage[]': KeyValueStorageArray,
+  'KeyValue': StorageKeyValue,
+  'StorageKeyValue': StorageKeyValue,
+  'StorageKeyValue[]': StorageKeyValueArray,
   'MisbehaviorReport': Unknown,
   'ParachainId': Amount,
   'PropIndex': Amount,
@@ -46,7 +46,7 @@ const components: ComponentMap = {
   'VoteThreshold': VoteThreshold
 };
 
-export default function findComponent (type: Param$Types, overrides: ComponentMap = {}): React.ComponentType<Props> {
+function getFromMap (type: Param$Types, overrides: ComponentMap): React.ComponentType<Props> | [React.ComponentType<Props>, React.ComponentType<Props>] {
   if (Array.isArray(type)) {
     // Special case for components where we have a specific override formatter
     if (type.length === 1) {
@@ -60,3 +60,11 @@ export default function findComponent (type: Param$Types, overrides: ComponentMa
 
   return overrides[type] || components[type] || Unknown;
 }
+
+export default function findComponent (type: Param$Types, overrides: ComponentMap = {}, isDisabled: boolean = false): React.ComponentType<Props> {
+  const component = getFromMap(type, overrides);
+
+  return Array.isArray(component)
+    ? component[0]
+    : component;
+}

+ 5 - 3
packages/ui-app/src/Params/Param/index.tsx

@@ -14,6 +14,7 @@ import translate from '../../translate';
 import findComponent from './findComponent';
 
 type Props = I18nProps & BaseProps & {
+  isDisabled?: boolean,
   overrides?: ComponentMap
 };
 
@@ -26,11 +27,11 @@ class ParamComponent extends React.PureComponent<Props, State> {
     Component: null
   };
 
-  static getDerivedStateFromProps ({ defaultValue: { type }, overrides }: Props): State {
+  static getDerivedStateFromProps ({ defaultValue: { type }, isDisabled, overrides }: Props): State {
     return {
       Component: !type
         ? null
-        : findComponent(type, overrides)
+        : findComponent(type, overrides, isDisabled)
     } as State;
   }
 
@@ -41,7 +42,7 @@ class ParamComponent extends React.PureComponent<Props, State> {
       return null;
     }
 
-    const { className, defaultValue, name, onChange, style } = this.props;
+    const { className, defaultValue, isDisabled, name, onChange, style } = this.props;
     const type = typeToString(defaultValue.type);
 
     return (
@@ -49,6 +50,7 @@ class ParamComponent extends React.PureComponent<Props, State> {
         className={classes('ui--Param', className)}
         defaultValue={defaultValue}
         key={`${name}:${type}`}
+        isDisabled={isDisabled}
         label={`${name}: ${type}`}
         name={name}
         onChange={onChange}

+ 27 - 12
packages/ui-app/src/Params/index.tsx

@@ -16,9 +16,11 @@ import Param from './Param';
 import createValues from './values';
 
 type Props<S> = I18nProps & {
+  isDisabled?: boolean,
   item: S,
-  onChange: (value: RawParams) => void,
-  overrides?: ComponentMap
+  onChange?: (value: RawParams) => void,
+  overrides?: ComponentMap,
+  values?: RawParams
 };
 
 type State<S> = {
@@ -40,7 +42,9 @@ class Params<T, S extends SectionItem<T>> extends React.PureComponent<Props<S>,
   }
 
   static getDerivedStateFromProps (props: Props<any>, { item, onChangeParam }: State<any>): State<any> | null {
-    if (item && item.name === props.item.name && item.section === props.item.section) {
+    const isSame = item && item.name === props.item.name && item.section === props.item.section;
+
+    if (props.isDisabled || isSame) {
       return null;
     }
 
@@ -61,19 +65,19 @@ class Params<T, S extends SectionItem<T>> extends React.PureComponent<Props<S>,
 
    // NOTE This is needed in the case where the item changes, i.e. the values get initialised and we need to alert the parent that we have new values
   componentDidUpdate (prevProps: Props<S>, prevState: State<S>) {
-    const { onChange } = this.props;
+    const { onChange, isDisabled } = this.props;
     const { values } = this.state;
 
-    if (prevState.values !== values) {
-      onChange(values);
+    if (!isDisabled && prevState.values !== values) {
+      onChange && onChange(values);
     }
   }
 
   render () {
-    const { className, item: { params }, overrides, style } = this.props;
-    const { handlers, values } = this.state;
+    const { className, isDisabled, item: { params }, overrides, style } = this.props;
+    const { handlers = [], values = this.props.values } = this.state;
 
-    if (values.length === 0 || params.length === 0) {
+    if (!values || values.length === 0 || params.length === 0) {
       return null;
     }
 
@@ -86,6 +90,7 @@ class Params<T, S extends SectionItem<T>> extends React.PureComponent<Props<S>,
           {params.map(({ name }, index) => (
             <Param
               defaultValue={values[index]}
+              isDisabled={isDisabled}
               key={`${name}:${name}:${index}`}
               name={name}
               onChange={handlers[index]}
@@ -97,7 +102,13 @@ class Params<T, S extends SectionItem<T>> extends React.PureComponent<Props<S>,
     );
   }
 
-  onChangeParam = (at: number, { isValid = false, value }: RawParam$OnChange$Value): void => {
+  private onChangeParam = (at: number, { isValid = false, value }: RawParam$OnChange$Value): void => {
+    const { isDisabled } = this.props;
+
+    if (isDisabled) {
+      return;
+    }
+
     this.setState(
       (prevState: State<S>): State<S> => ({
         values: prevState.values.map((prev, index) =>
@@ -116,9 +127,13 @@ class Params<T, S extends SectionItem<T>> extends React.PureComponent<Props<S>,
 
   triggerUpdate = (): void => {
     const { values } = this.state;
-    const { onChange } = this.props;
+    const { onChange, isDisabled } = this.props;
+
+    if (isDisabled) {
+      return;
+    }
 
-    onChange(values);
+    onChange && onChange(values);
   }
 }
 

+ 1 - 1
packages/ui-app/src/Params/initValue.ts

@@ -49,7 +49,7 @@ export default function getInitValue (type: Param$Types): RawParam$Value | Array
     case 'Hash':
     case 'Header':
     case 'KeyValue':
-    case 'KeyValueStorage':
+    case 'StorageKeyValue':
     case 'MisbehaviorReport':
     case 'Proposal':
     case 'Signature':

+ 3 - 2
packages/ui-app/src/Params/types.d.ts

@@ -25,12 +25,13 @@ export type RawParams = Array<RawParam>;
 export type BaseProps = BareProps & {
   defaultValue: RawParam,
   name: string,
-  onChange: RawParam$OnChange
+  onChange?: RawParam$OnChange
 };
 
 export type Props = BaseProps & {
   isDisabled?: boolean,
   isError?: boolean,
+  isReadOnly?: boolean,
   label: string,
   withLabel?: boolean
 };
@@ -38,5 +39,5 @@ export type Props = BaseProps & {
 export type Size = 'full' | 'large' | 'medium' | 'small';
 
 export type ComponentMap = {
-  [index: string]: React.ComponentType<Props> // Param$Type
+  [index: string]: React.ComponentType<Props> | [React.ComponentType<Props>, React.ComponentType<Props>] // Param$Type
 };

+ 6 - 0
packages/ui-app/src/styles/app.css

@@ -16,3 +16,9 @@ body {
   font-family: sans-serif;
   height: 100%;
 }
+
+h1 {
+  color: rgba(0, 0, 0, .6);
+  font-weight: 400;
+  text-transform: lowercase;
+}

+ 13 - 0
packages/ui-app/src/styles/components.css

@@ -32,6 +32,11 @@
 }
 
 .ui--AddressMini {
+  display: inline-block;
+  padding: 0 0.25rem 0 0;
+}
+
+.ui--AddressMini.padded {
   display: inline-block;
   padding: 0.25rem;
 }
@@ -137,6 +142,14 @@
   padding-top: 0;
 }
 
+.ui--Chart {
+  position: relative;
+  display: inline-block;
+  padding: 1em 1em 0;
+  height: 15vw;
+  width: 15vw;
+}
+
 .ui--CardSummary-large {
   font-size: 3rem;
   line-height: 3rem;

+ 3 - 3
packages/ui-react-rx/package.json

@@ -36,9 +36,9 @@
   },
   "dependencies": {
     "@babel/runtime": "^7.0.0-beta.51",
-    "@polkadot/api-rx": "^0.28.9",
-    "@polkadot/extrinsics": "^0.28.9",
-    "@polkadot/storage": "^0.28.9",
+    "@polkadot/api-rx": "^0.28.11",
+    "@polkadot/extrinsics": "^0.28.11",
+    "@polkadot/storage": "^0.28.11",
     "rxjs-compat": "^6.2.2"
   },
   "devDependencies": {

+ 241 - 41
packages/ui-react-rx/src/ApiObservable.ts → packages/ui-react-rx/src/ApiObservable/index.ts

@@ -5,20 +5,25 @@
 import { Header } from '@polkadot/primitives/header';
 import { RxApiInterface, RxApiInterface$Method } from '@polkadot/api-rx/types';
 import { Interfaces } from '@polkadot/jsonrpc/types';
-import { SectionItem } from '@polkadot/params/types';
+import { ExtrinsicDecoded, SectionItem } from '@polkadot/params/types';
 import { Storages } from '@polkadot/storage/types';
-import { ExtendedBalance, ExtendedBalanceMap, ObservableApiInterface, KeyWithParams } from './types';
+import { RxBalance, RxBalanceMap, RxProposal, RxProposalDeposits, RxReferendum, ObservableApiInterface, KeyWithParams, RxReferendumVote } from './types';
 
 import BN from 'bn.js';
-import { Observable, combineLatest } from 'rxjs';
-import { concatMap, defaultIfEmpty, map } from 'rxjs/operators';
+import { EMPTY, Observable, combineLatest } from 'rxjs';
+import { switchMap, defaultIfEmpty, map } from 'rxjs/operators';
 import storage from '@polkadot/storage';
 import assert from '@polkadot/util/assert';
+import isUndefined from '@polkadot/util/is/undefined';
 
 type OptBN = BN | undefined;
 type OptDate = Date | undefined;
 type MapFn<R, T> = (combined: R) => T;
 
+type ResultReferendum = [BN, ExtrinsicDecoded, number];
+type ResultProposal = [BN, ExtrinsicDecoded, string];
+type ResultProposalDeposits = [BN, Array<string>];
+
 const defaultMapFn = (result: any): any =>
   result;
 
@@ -52,31 +57,218 @@ export default class ObservableApi implements ObservableApiInterface {
   rawStorage = <T> (key: SectionItem<Storages>, ...params: Array<any>): Observable<T> => {
     return this
       .rawStorageMulti([key, ...params] as KeyWithParams)
-      .pipe(map(([result]: Array<T>): T =>
-        result
-      ));
+      .pipe(
+        map(([result]: Array<T>): T =>
+          result
+        )
+      );
   }
 
   rawStorageMulti = <T> (...keys: Array<KeyWithParams>): Observable<T> => {
     return this.api.state
       .subscribeStorage(keys)
-      .pipe(map((result?: any) =>
-        result || []
-      ));
+      .pipe(
+        map((result?: any) =>
+          isUndefined(result)
+            ? []
+            : result
+        )
+      );
   }
 
   bestNumber = (): Observable<OptBN> => {
-    return this.chainNewHead().pipe(
-      map((header?: Header): OptBN =>
-        header && header.number
-          ? header.number
-          : undefined
+    return this
+      .chainNewHead()
+      .pipe(
+        map((header?: Header): OptBN =>
+          header && header.number
+            ? header.number
+            : undefined
+        )
+      );
+  }
+
+  chainNewHead = (): Observable<Header | undefined> => {
+    return this.api.chain.subscribeNewHead().pipe(
+      defaultIfEmpty()
+    );
+  }
+
+  democracyLaunchPeriod = (): Observable<OptBN> => {
+    return this.rawStorage(storage.democracy.public.launchPeriod);
+  }
+
+  democracyNextTally = (): Observable<OptBN> => {
+    return this.rawStorage(storage.democracy.public.nextTally);
+  }
+
+  democracyProposals = (): Observable<Array<RxProposal>> => {
+    return this
+      .rawStorage(storage.democracy.public.proposals)
+      .pipe(
+        map((proposals: Array<ResultProposal> = []) =>
+          proposals
+            .map((result: ResultProposal): RxProposal | undefined =>
+              result && result[1]
+                ? {
+                  address: result[2],
+                  id: result[0],
+                  proposal: result[1]
+                }
+                : undefined
+            )
+            .filter((proposal) =>
+              proposal
+            )
+        )
+    );
+  }
+
+  democracyProposalCount = (): Observable<number> => {
+    return this
+      .democracyProposals()
+      .pipe(
+        map((proposals: Array<RxProposal>) =>
+          proposals.length
+        )
+      );
+  }
+
+  democracyProposalDeposits = (proposalId: BN): Observable<RxProposalDeposits | undefined> => {
+    return this
+      .rawStorage(storage.democracy.public.depositOf, proposalId)
+      .pipe(
+        map((result: ResultProposalDeposits): RxProposalDeposits | undefined =>
+          result && result[0]
+            ? {
+              addresses: result[1] || [],
+              balance: result[0]
+            }
+            : undefined
+        )
+      );
+  }
+
+  democracyReferendumCount = (): Observable<OptBN> => {
+    return this.rawStorage(storage.democracy.public.referendumCount);
+  }
+
+  democracyReferendumInfoOf = (referendumId: BN | number): Observable<RxReferendum> => {
+    return this
+      .rawStorage(storage.democracy.public.referendumInfoOf, referendumId)
+      .pipe(
+        map((result: ResultReferendum) =>
+          result && result[1]
+            ? {
+              blockNumber: result[0],
+              id: new BN(referendumId),
+              proposal: result[1],
+              voteThreshold: result[2]
+            }
+            : undefined
+        )
+      );
+  }
+
+  democracyReferendumInfos = (referendumIds: Array<number>): Observable<Array<RxReferendum>> => {
+    return this.combine(
+      referendumIds.map((referendumId) =>
+        this.democracyReferendumInfoOf(referendumId)
+      ),
+      (referendums: Array<RxReferendum> = []): Array<RxReferendum> =>
+        referendums.filter((referendum) =>
+          referendum
+        )
+    );
+  }
+
+  democracyReferendumVoters = (referendumId: BN): Observable<Array<RxReferendumVote>> => {
+    return this.combine(
+      [
+        this.democacyVotersFor(referendumId),
+        this.democracyVotersBalancesOf(referendumId),
+        this.democracyVotersVotesOf(referendumId)
+      ],
+      ([voters, balances, votes]: [Array<string>, Array<BN>, Array<boolean>]): Array<RxReferendumVote> =>
+        voters.map((address, index): RxReferendumVote => ({
+          address,
+          balance: balances[index] || new BN(0),
+          vote: votes[index] || false
+        }))
+    );
+  }
+
+  democracyReferendums = (): Observable<Array<RxReferendum>> => {
+    return this.combine(
+      [
+        this.democracyReferendumCount(),
+        this.democracyNextTally()
+      ]
+    ).pipe(
+      switchMap(([referendumCount, nextTally]: [OptBN, OptBN]): Observable<Array<RxReferendum>> =>
+        referendumCount && nextTally && referendumCount.gt(nextTally) && referendumCount.gtn(0)
+          ? this.democracyReferendumInfos(
+            [...Array(referendumCount.sub(nextTally).toNumber())].map((_, i) =>
+              nextTally.addn(i).toNumber()
+            )
+          )
+          : EMPTY
+      ),
+      defaultIfEmpty([])
+    );
+  }
+
+  democacyVoteOf = (index: BN, address: string): Observable<boolean> => {
+    return this.rawStorage(storage.democracy.public.voteOf, index, address);
+  }
+
+  democracyVotesOf = (index: BN, addresses: Array<string>): Observable<boolean> => {
+    return this.combine(
+      addresses.map((address) =>
+        this.democacyVoteOf(index, address)
       )
     );
   }
 
-  chainNewHead = (): Observable<Header | undefined> => {
-    return this.api.chain.subscribeNewHead();
+  democacyVotersFor = (index: BN): Observable<Array<string>> => {
+    return this
+      .rawStorage(storage.democracy.public.votersFor, index)
+      .pipe(
+        map((voters: Array<string> = []) =>
+          voters
+        )
+      );
+  }
+
+  democracyVotersBalancesOf = (referendumId: BN): Observable<Array<BN>> => {
+    return this
+      .democacyVotersFor(referendumId)
+      .pipe(
+        switchMap((voters: Array<string> = []) =>
+          this.votingBalances(...voters)
+        ),
+        defaultIfEmpty([]),
+        map((balances: Array<RxBalance>) =>
+          balances.map(({ votingBalance }) =>
+            votingBalance
+          )
+        )
+      );
+  }
+
+  democracyVotersVotesOf = (referendumId: BN): Observable<Array<boolean>> => {
+    return this
+      .democacyVotersFor(referendumId)
+      .pipe(
+        switchMap((voters: Array<string> = []) =>
+          this.democracyVotesOf(referendumId, voters)
+        ),
+        defaultIfEmpty([])
+      );
+  }
+
+  democracyVotingPeriod = (): Observable<OptBN> => {
+    return this.rawStorage(storage.democracy.public.votingPeriod);
   }
 
   eraBlockLength = (): Observable<OptBN> => {
@@ -231,17 +423,21 @@ export default class ObservableApi implements ObservableApiInterface {
   sessionValidators = (): Observable<Array<string>> => {
     return this
       .rawStorage(storage.session.public.validators)
-      .pipe(map((validators: Array<string> = []) =>
-        validators
-      ));
+      .pipe(
+        map((validators: Array<string> = []) =>
+          validators
+        )
+      );
   }
 
   stakingIntentions = (): Observable<Array<string>> => {
     return this
       .rawStorage(storage.staking.public.intentions)
-      .pipe(map((intentions: Array<string> = []) =>
-        intentions
-      ));
+      .pipe(
+        map((intentions: Array<string> = []) =>
+          intentions
+        )
+      );
   }
 
   stakingFreeBalanceOf = (address: string): Observable<OptBN> => {
@@ -251,9 +447,11 @@ export default class ObservableApi implements ObservableApiInterface {
   stakingNominatorsFor = (address: string): Observable<Array<string>> => {
     return this
       .rawStorage(storage.staking.public.nominatorsFor, address)
-      .pipe(map((nominators: Array<string> = []) =>
-        nominators
-      ));
+      .pipe(
+        map((nominators: Array<string> = []) =>
+          nominators
+        )
+      );
   }
 
   stakingNominating = (address: string): Observable<string | undefined> => {
@@ -276,14 +474,14 @@ export default class ObservableApi implements ObservableApiInterface {
     return this.rawStorage(storage.system.public.accountIndexOf, address);
   }
 
-  validatingBalance = (address: string): Observable<ExtendedBalance> => {
+  validatingBalance = (address: string): Observable<RxBalance> => {
     return this.combine(
       [
         this.votingBalance(address),
         this.votingBalancesNominatorsFor(address)
       ],
-      ([balance, nominators = []]: [ExtendedBalance, Array<ExtendedBalance>]): ExtendedBalance => {
-        const nominatedBalance = nominators.reduce((total, nominator: ExtendedBalance) => {
+      ([balance, nominators = []]: [RxBalance, Array<RxBalance>]): RxBalance => {
+        const nominatedBalance = nominators.reduce((total, nominator: RxBalance) => {
           return total.add(nominator.votingBalance);
         }, new BN(0));
 
@@ -299,27 +497,27 @@ export default class ObservableApi implements ObservableApiInterface {
     );
   }
 
-  validatingBalances = (...addresses: Array<string>): Observable<ExtendedBalanceMap> => {
+  validatingBalances = (...addresses: Array<string>): Observable<RxBalanceMap> => {
     return this.combine(
       addresses.map((address) =>
         this.validatingBalance(address)
       ),
-      (result: Array<ExtendedBalance>): ExtendedBalanceMap =>
+      (result: Array<RxBalance>): RxBalanceMap =>
         result.reduce((balances, balance) => {
           balances[balance.address] = balance;
 
           return balances;
-        }, {} as ExtendedBalanceMap)
+        }, {} as RxBalanceMap)
     );
   }
 
-  votingBalance = (address: string): Observable<ExtendedBalance> => {
+  votingBalance = (address: string): Observable<RxBalance> => {
     return this.combine(
       [
         this.stakingFreeBalanceOf(address),
         this.stakingReservedBalanceOf(address)
       ],
-      ([freeBalance = new BN(0), reservedBalance = new BN(0)]: [OptBN, OptBN]): ExtendedBalance => ({
+      ([freeBalance = new BN(0), reservedBalance = new BN(0)]: [OptBN, OptBN]): RxBalance => ({
         address,
         freeBalance,
         nominatedBalance: new BN(0),
@@ -331,15 +529,17 @@ export default class ObservableApi implements ObservableApiInterface {
   }
 
   votingBalancesNominatorsFor = (address: string) => {
-    return this.stakingNominatorsFor(address).pipe(
-      concatMap((nominators: Array<string>) =>
-        this.votingBalances(...nominators)
-      ),
-      defaultIfEmpty([])
-    );
+    return this
+      .stakingNominatorsFor(address)
+      .pipe(
+        switchMap((nominators: Array<string>) =>
+          this.votingBalances(...nominators)
+        ),
+        defaultIfEmpty([])
+      );
   }
 
-  votingBalances = (...addresses: Array<string>): Observable<ExtendedBalance[]> => {
+  votingBalances = (...addresses: Array<string>): Observable<RxBalance[]> => {
     return this.combine(
       addresses.map((address) =>
         this.votingBalance(address)

+ 92 - 0
packages/ui-react-rx/src/ApiObservable/types.d.ts

@@ -0,0 +1,92 @@
+// 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 BN from 'bn.js';
+import { Observable } from 'rxjs';
+import { Interfaces } from '@polkadot/jsonrpc/types';
+import { ExtrinsicDecoded, SectionItem } from '@polkadot/params/types';
+import { Header } from '@polkadot/primitives/header';
+import { Storages } from '@polkadot/storage/types';
+
+export type RxBalance = {
+  address: string,
+  freeBalance: BN,
+  nominatedBalance: BN,
+  reservedBalance: BN,
+  votingBalance: BN,
+  stakingBalance: BN,
+  nominators?: Array<RxBalance>
+}
+
+export type RxProposal = {
+  address: string,
+  id: BN,
+  proposal: ExtrinsicDecoded
+};
+
+export type RxProposalDeposits = {
+  balance: BN,
+  addresses: Array<string>
+}
+
+export type RxReferendum = {
+  blockNumber: BN,
+  id: BN,
+  proposal: ExtrinsicDecoded,
+  voteThreshold: number
+}
+
+export type RxReferendumVote = {
+  address: string,
+  balance: BN,
+  vote: boolean
+};
+
+export type RxBalanceMap = {
+  [index: string]: RxBalance
+}
+
+export type KeyWithParams = [SectionItem<Storages>, any];
+
+export interface ObservableApiInterface {
+  rawCall: <T> ({ name, section }: SectionItem<Interfaces>, ...params: Array<any>) => Observable<T>,
+  rawStorage: <T> (key: SectionItem<Storages>, ...params: Array<any>) => Observable<T>,
+  rawStorageMulti: <T> (...keys: Array<[SectionItem<Storages>, any]>) => Observable<T>,
+  bestNumber: () => Observable<BN | undefined>,
+  chainNewHead: () => Observable<Header | undefined>,
+  democracyLaunchPeriod: () => Observable<BN | undefined>,
+  democracyNextTally: () => Observable<BN | undefined>,
+  democracyProposalCount: () => Observable<number>,
+  democracyProposalDeposits: (index: BN) => Observable<RxProposalDeposits | undefined>,
+  democracyProposals: () => Observable<Array<RxProposal>>,
+  democracyReferendumCount: () => Observable<BN | undefined>,
+  democracyReferendums: () => Observable<Array<RxReferendum>>,
+  democracyReferendumVoters:(index: BN) => Observable<Array<RxReferendumVote>>,
+  democracyVotingPeriod: () => Observable<BN | undefined>,
+  eraBlockLength: () => Observable<BN | undefined>,
+  eraBlockProgress: () => Observable<BN | undefined>,
+  eraBlockRemaining: () => Observable<BN | undefined>,
+  sessionBlockProgress: () => Observable<BN | undefined>,
+  sessionBlockRemaining: () => Observable<BN | undefined>,
+  sessionBrokenPercentLate: () => Observable<BN | undefined>,
+  sessionBrokenValue: () => Observable<BN | undefined>,
+  sessionLength: () => Observable<BN | undefined>,
+  sessionTimeExpected: () => Observable<BN | undefined>,
+  sessionTimeRemaining: () => Observable<BN | undefined>,
+  sessionValidators: () => Observable<Array<string>>,
+  stakingFreeBalanceOf: (address: string) => Observable<BN | undefined>,
+  stakingIntentions: () => Observable<Array<string>>,
+  stakingNominatorsFor: (address: string) => Observable<Array<string>>,
+  stakingNominating: (address: string) => Observable<string | undefined>,
+  stakingReservedBalanceOf: (address: string) => Observable<BN | undefined>,
+  systemAccountIndexOf: (address: string) => Observable<BN | undefined>,
+  timestampBlockPeriod: () => Observable<BN | undefined>,
+  timestampNow: () => Observable<Date | undefined>,
+  validatingBalance: (address: string) => Observable<RxBalance>,
+  validatingBalances: (...addresses: Array<string>) => Observable<RxBalanceMap>,
+  votingBalance: (address: string) => Observable<RxBalance>,
+  votingBalances: (...addresses: Array<string>) => Observable<RxBalance[]>
+}
+
+export type ObservableApiNames = keyof ObservableApiInterface;

+ 1 - 49
packages/ui-react-rx/src/types.d.ts

@@ -10,61 +10,13 @@ import { Interfaces } from '@polkadot/jsonrpc/types';
 import { EncodingVersions, SectionItem } from '@polkadot/params/types';
 import { Header } from '@polkadot/primitives/header';
 import { Storages, Storage$Key$Value } from '@polkadot/storage/types';
+import { ObservableApiInterface } from './ApiObservable/types';
 
 export type BareProps = {
   className?: string,
   style?: { [index: string]: any }
 };
 
-export type ExtendedBalance = {
-  address: string,
-  freeBalance: BN,
-  nominatedBalance: BN,
-  reservedBalance: BN,
-  votingBalance: BN,
-  stakingBalance: BN,
-  nominators?: Array<ExtendedBalance>
-}
-
-export type ExtendedBalanceMap = {
-  [index: string]: ExtendedBalance
-}
-
-export type KeyWithParams = [SectionItem<Storages>, any];
-
-export interface ObservableApiInterface {
-  rawCall: <T> ({ name, section }: SectionItem<Interfaces>, ...params: Array<any>) => Observable<T>,
-  rawStorage: <T> (key: SectionItem<Storages>, ...params: Array<any>) => Observable<T>,
-  rawStorageMulti: <T> (...keys: Array<[SectionItem<Storages>, any]>) => Observable<T>,
-  bestNumber: () => Observable<BN | undefined>,
-  chainNewHead: () => Observable<Header | undefined>,
-  eraBlockLength: () => Observable<BN | undefined>,
-  eraBlockProgress: () => Observable<BN | undefined>,
-  eraBlockRemaining: () => Observable<BN | undefined>,
-  sessionBlockProgress: () => Observable<BN | undefined>,
-  sessionBlockRemaining: () => Observable<BN | undefined>,
-  sessionBrokenPercentLate: () => Observable<BN | undefined>,
-  sessionBrokenValue: () => Observable<BN | undefined>,
-  sessionLength: () => Observable<BN | undefined>,
-  sessionTimeExpected: () => Observable<BN | undefined>,
-  sessionTimeRemaining: () => Observable<BN | undefined>,
-  sessionValidators: () => Observable<Array<string>>,
-  stakingFreeBalanceOf: (address: string) => Observable<BN | undefined>,
-  stakingIntentions: () => Observable<Array<string>>,
-  stakingNominatorsFor: (address: string) => Observable<Array<string>>,
-  stakingNominating: (address: string) => Observable<string | undefined>,
-  stakingReservedBalanceOf: (address: string) => Observable<BN | undefined>,
-  systemAccountIndexOf: (address: string) => Observable<BN | undefined>,
-  timestampBlockPeriod: () => Observable<BN | undefined>,
-  timestampNow: () => Observable<Date | undefined>,
-  validatingBalance: (address: string) => Observable<ExtendedBalance>,
-  validatingBalances: (...addresses: Array<string>) => Observable<ExtendedBalanceMap>,
-  votingBalance: (address: string) => Observable<ExtendedBalance>,
-  votingBalances: (...addresses: Array<string>) => Observable<ExtendedBalance[]>
-}
-
-export type ObservableApiNames = keyof ObservableApiInterface;
-
 export type ApiProps = {
   api: RxApiInterface,
   apiConnected: boolean,

+ 2 - 3
packages/ui-react-rx/src/with/observable.tsx

@@ -4,7 +4,8 @@
 
 // TODO: Lots of duplicated code between this and withObservable, surely there ois a better way of doing this?
 
-import { RxProps, ObservableApiNames } from '../types';
+import { ObservableApiNames } from '../ApiObservable/types';
+import { RxProps } from '../types';
 import { HOC, Options, DefaultProps, RenderFn } from './types';
 
 import React from 'react';
@@ -24,8 +25,6 @@ type State<T> = RxProps<T> & {
 // FIXME proper types for attributes
 
 export default function withObservable<T> (observable: ObservableApiNames, { onChange, params = [], paramProp = 'params', propName = observable, transform = echoTransform }: Options<T> = {}): HOC<T> {
-  console.log('observable', observable, paramProp);
-
   return (Inner: React.ComponentType<any>, defaultProps: DefaultProps<T> = {}, render?: RenderFn): React.ComponentType<any> => {
     class WithObservable extends React.Component<any, State<T>> {
       state: State<T>;

+ 2 - 1
packages/ui-react-rx/src/with/observableDiv.ts

@@ -2,7 +2,8 @@
 // This software may be modified and distributed under the terms
 // of the ISC license. See the LICENSE file for details.
 
-import { BaseProps, ObservableApiNames } from '../types';
+import { ObservableApiNames } from '../ApiObservable/types';
+import { BaseProps } from '../types';
 import { ComponentRenderer, DefaultProps, RenderFn, Options } from './types';
 
 import Div from '../Div';

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

@@ -16,7 +16,7 @@
   },
   "dependencies": {
     "@babel/runtime": "^7.0.0-beta.51",
-    "@polkadot/extrinsics": "^0.28.9",
+    "@polkadot/extrinsics": "^0.28.11",
     "@polkadot/ui-app": "^0.19.30",
     "@polkadot/ui-keyring": "^0.19.30"
   },

+ 1 - 0
tsconfig.json

@@ -5,6 +5,7 @@
     "paths": {
       "@polkadot/app-accounts/*": [ "packages/app-accounts/src/*" ],
       "@polkadot/app-addresses/*": [ "packages/app-addresses/src/*" ],
+      "@polkadot/app-democracy/*": [ "packages/app-democracy/src/*" ],
       "@polkadot/app-explorer/*": [ "packages/app-explorer/src/*" ],
       "@polkadot/app-example/*": [ "packages/app-example/src/*" ],
       "@polkadot/app-extrinsics/*": [ "packages/app-extrinsics/src/*" ],

+ 80 - 45
yarn.lock

@@ -765,22 +765,22 @@
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.0.tgz#50c1e2260ac0ed9439a181de3725a0168d59c48a"
 
-"@polkadot/api-format@^0.28.9":
-  version "0.28.9"
-  resolved "https://registry.yarnpkg.com/@polkadot/api-format/-/api-format-0.28.9.tgz#1360e37534d21e871b1bee0d58a0a2ebb99ad753"
+"@polkadot/api-format@^0.28.11":
+  version "0.28.11"
+  resolved "https://registry.yarnpkg.com/@polkadot/api-format/-/api-format-0.28.11.tgz#7497f36d764016cd715795b6a62de62424d78c5b"
   dependencies:
     "@babel/runtime" "^7.0.0-beta.51"
-    "@polkadot/jsonrpc" "^0.28.9"
-    "@polkadot/primitives" "^0.28.9"
+    "@polkadot/jsonrpc" "^0.28.11"
+    "@polkadot/primitives" "^0.28.11"
     "@polkadot/util" "^0.28.1"
     "@polkadot/util-keyring" "^0.28.1"
 
-"@polkadot/api-provider@^0.28.9":
-  version "0.28.9"
-  resolved "https://registry.yarnpkg.com/@polkadot/api-provider/-/api-provider-0.28.9.tgz#642cc45d9248a61c7f80969290e19461c8f67047"
+"@polkadot/api-provider@^0.28.11":
+  version "0.28.11"
+  resolved "https://registry.yarnpkg.com/@polkadot/api-provider/-/api-provider-0.28.11.tgz#3d2ccbb039d84130e828e8a9883d4e5cdee10d99"
   dependencies:
     "@babel/runtime" "^7.0.0-beta.51"
-    "@polkadot/storage" "^0.28.9"
+    "@polkadot/storage" "^0.28.11"
     "@polkadot/util" "^0.28.1"
     "@polkadot/util-crypto" "^0.28.1"
     "@polkadot/util-keyring" "^0.28.1"
@@ -789,25 +789,25 @@
     isomorphic-fetch "^2.2.1"
     websocket "^1.0.25"
 
-"@polkadot/api-rx@^0.28.9":
-  version "0.28.9"
-  resolved "https://registry.yarnpkg.com/@polkadot/api-rx/-/api-rx-0.28.9.tgz#5ad6352ce42df7888c37baef40c2acf24e5a7d76"
+"@polkadot/api-rx@^0.28.11":
+  version "0.28.11"
+  resolved "https://registry.yarnpkg.com/@polkadot/api-rx/-/api-rx-0.28.11.tgz#5f0a057ea7b160b3242f12f153bff581278c9271"
   dependencies:
     "@babel/runtime" "^7.0.0-beta.51"
-    "@polkadot/api" "^0.28.9"
-    "@polkadot/api-provider" "^0.28.9"
+    "@polkadot/api" "^0.28.11"
+    "@polkadot/api-provider" "^0.28.11"
     "@types/rx" "^4.1.1"
     rxjs "^6.2.2"
 
-"@polkadot/api@^0.28.9":
-  version "0.28.9"
-  resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-0.28.9.tgz#86c2f2db81256ba4d101d37ca18c3fcaa076e0c7"
+"@polkadot/api@^0.28.11":
+  version "0.28.11"
+  resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-0.28.11.tgz#46b2a7b395dabdc9cb47a769aec3c419a7c2e405"
   dependencies:
     "@babel/runtime" "^7.0.0-beta.51"
-    "@polkadot/api-format" "^0.28.9"
-    "@polkadot/api-provider" "^0.28.9"
-    "@polkadot/jsonrpc" "^0.28.9"
-    "@polkadot/params" "^0.28.9"
+    "@polkadot/api-format" "^0.28.11"
+    "@polkadot/api-provider" "^0.28.11"
+    "@polkadot/jsonrpc" "^0.28.11"
+    "@polkadot/params" "^0.28.11"
     "@polkadot/util" "^0.28.1"
 
 "@polkadot/dev-react@^0.20.12":
@@ -877,48 +877,48 @@
     typedoc "^0.11.1"
     typescript "^2.9.2"
 
-"@polkadot/extrinsics@^0.28.9":
-  version "0.28.9"
-  resolved "https://registry.yarnpkg.com/@polkadot/extrinsics/-/extrinsics-0.28.9.tgz#b7804179153c29e793a9d549b566f23a13f98558"
+"@polkadot/extrinsics@^0.28.11":
+  version "0.28.11"
+  resolved "https://registry.yarnpkg.com/@polkadot/extrinsics/-/extrinsics-0.28.11.tgz#834c04b58dbdeee9750bdca7a2cc125c2411bb2e"
   dependencies:
     "@babel/runtime" "^7.0.0-beta.51"
-    "@polkadot/params" "^0.28.9"
-    "@polkadot/primitives" "^0.28.9"
+    "@polkadot/params" "^0.28.11"
+    "@polkadot/primitives" "^0.28.11"
     "@polkadot/util" "^0.28.1"
 
-"@polkadot/jsonrpc@^0.28.9":
-  version "0.28.9"
-  resolved "https://registry.yarnpkg.com/@polkadot/jsonrpc/-/jsonrpc-0.28.9.tgz#564116660f703084048139c3bda7f01c979e55b4"
+"@polkadot/jsonrpc@^0.28.11":
+  version "0.28.11"
+  resolved "https://registry.yarnpkg.com/@polkadot/jsonrpc/-/jsonrpc-0.28.11.tgz#0687b93077be6ed3dc16f1d8937918ed9647b5e8"
   dependencies:
-    "@polkadot/params" "^0.28.9"
+    "@polkadot/params" "^0.28.11"
     babel-runtime "^6.26.0"
 
-"@polkadot/params@^0.28.9":
-  version "0.28.9"
-  resolved "https://registry.yarnpkg.com/@polkadot/params/-/params-0.28.9.tgz#220888b70d147c71f195d06b1e38525c520e04aa"
+"@polkadot/params@^0.28.11":
+  version "0.28.11"
+  resolved "https://registry.yarnpkg.com/@polkadot/params/-/params-0.28.11.tgz#9dea2efa3c6b4725547fd6dd8b3c2312d1d226d7"
   dependencies:
     "@babel/runtime" "^7.0.0-beta.51"
-    "@polkadot/extrinsics" "^0.28.9"
-    "@polkadot/params" "^0.28.9"
-    "@polkadot/primitives" "^0.28.9"
+    "@polkadot/extrinsics" "^0.28.11"
+    "@polkadot/params" "^0.28.11"
+    "@polkadot/primitives" "^0.28.11"
     "@polkadot/util" "^0.28.1"
     "@polkadot/util-keyring" "^0.28.1"
 
-"@polkadot/primitives@^0.28.9":
-  version "0.28.9"
-  resolved "https://registry.yarnpkg.com/@polkadot/primitives/-/primitives-0.28.9.tgz#e451adc1faf78b397f7020f0f036d8542e48aab0"
+"@polkadot/primitives@^0.28.11":
+  version "0.28.11"
+  resolved "https://registry.yarnpkg.com/@polkadot/primitives/-/primitives-0.28.11.tgz#4c32f181a779f002d9def60cecf46237034ef270"
   dependencies:
     "@babel/runtime" "^7.0.0-beta.51"
     "@polkadot/trie-hash" "^0.28.1"
     "@polkadot/util" "^0.28.1"
 
-"@polkadot/storage@^0.28.9":
-  version "0.28.9"
-  resolved "https://registry.yarnpkg.com/@polkadot/storage/-/storage-0.28.9.tgz#2ac372d64ba1f92310da88476a1be16f92a30443"
+"@polkadot/storage@^0.28.11":
+  version "0.28.11"
+  resolved "https://registry.yarnpkg.com/@polkadot/storage/-/storage-0.28.11.tgz#c388142dcc409fb151512f2c34b6d191656c9bd3"
   dependencies:
     "@babel/runtime" "^7.0.0-beta.51"
-    "@polkadot/params" "^0.28.9"
-    "@polkadot/primitives" "^0.28.9"
+    "@polkadot/params" "^0.28.11"
+    "@polkadot/primitives" "^0.28.11"
     "@polkadot/util" "^0.28.1"
     "@polkadot/util-crypto" "^0.28.1"
     "@polkadot/util-keyring" "^0.28.1"
@@ -1021,6 +1021,10 @@
   dependencies:
     "@types/base-x" "*"
 
+"@types/chart.js@^2.7.30":
+  version "2.7.30"
+  resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.7.30.tgz#b3a973d4790ff5cbf1352ee756f4bfde5ae1cd27"
+
 "@types/color-convert@*":
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-1.9.0.tgz#bfa8203e41e7c65471e9841d7e306a7cd8b5172d"
@@ -2862,6 +2866,26 @@ chardet@^0.4.0:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
 
+chart.js@^2.7.2:
+  version "2.7.2"
+  resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.7.2.tgz#3c9fde4dc5b95608211bdefeda7e5d33dffa5714"
+  dependencies:
+    chartjs-color "^2.1.0"
+    moment "^2.10.2"
+
+chartjs-color-string@^0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz#8d3752d8581d86687c35bfe2cb80ac5213ceb8c1"
+  dependencies:
+    color-name "^1.0.0"
+
+chartjs-color@^2.1.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.2.0.tgz#84a2fb755787ed85c39dd6dd8c7b1d88429baeae"
+  dependencies:
+    chartjs-color-string "^0.5.0"
+    color-convert "^0.5.3"
+
 cheerio@^1.0.0-rc.2:
   version "1.0.0-rc.2"
   resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db"
@@ -3092,6 +3116,10 @@ collection-visit@^1.0.0:
     map-visit "^1.0.0"
     object-visit "^1.0.0"
 
+color-convert@^0.5.3:
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd"
+
 color-convert@^1.3.0, color-convert@^1.8.2, color-convert@^1.9.0, color-convert@^1.9.1:
   version "1.9.2"
   resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.2.tgz#49881b8fba67df12a96bdf3f56c0aab9e7913147"
@@ -7570,7 +7598,7 @@ modify-values@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
 
-moment@^2.22.2, moment@^2.6.0:
+moment@^2.10.2, moment@^2.22.2, moment@^2.6.0:
   version "2.22.2"
   resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
 
@@ -9225,6 +9253,13 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
+react-chartjs-2@^2.7.4:
+  version "2.7.4"
+  resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-2.7.4.tgz#e41ea4e81491dc78347111126a48e96ee57db1a6"
+  dependencies:
+    lodash "^4.17.4"
+    prop-types "^15.5.8"
+
 react-copy-to-clipboard@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e"