From cf8a24a8daf3cf1848566c24e35d00d774df94d3 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Thu, 2 Aug 2018 17:15:07 +0200 Subject: [PATCH] Staking overview & actions (#171) * Staking overview & actions * Show session countdown * Accounts with stake/Unstake buttons * Small layout tweaks * Cleanup multi-params from storage * Submission/click works... * Cleanups * Render split * Case sensitivity * Doc links * Icon to indicate validating --- README.md | 1 + jest.config.js | 2 +- packages/app-accounts/src/Address.tsx | 19 ++- packages/app-staking/LICENSE | 15 ++ packages/app-staking/README.md | 1 + packages/app-staking/package.json | 21 +++ packages/app-staking/src/Account.tsx | 145 ++++++++++++++++++ packages/app-staking/src/StakeList.tsx | 55 +++++++ packages/app-staking/src/Summary.tsx | 194 +++++++++++++++++++++++++ packages/app-staking/src/index.css | 58 ++++++++ packages/app-staking/src/index.tsx | 65 +++++++++ packages/app-staking/src/translate.ts | 7 + packages/apps/package.json | 1 + packages/apps/src/routing/index.ts | 3 +- packages/apps/src/routing/staking.ts | 20 +++ packages/apps/webpack.config.js | 2 +- packages/ui-keyring/src/account/all.ts | 16 ++ packages/ui-keyring/src/address/get.ts | 6 +- packages/ui-keyring/src/index.ts | 3 + packages/ui-keyring/src/types.d.ts | 1 + packages/ui-react-rx/src/with/multi.ts | 13 ++ packages/ui-signer/src/Queue.tsx | 37 ++++- packages/ui-signer/src/types.d.ts | 17 ++- tsconfig.json | 1 + yarn.lock | 4 +- 25 files changed, 689 insertions(+), 18 deletions(-) create mode 100644 packages/app-staking/LICENSE create mode 100644 packages/app-staking/README.md create mode 100644 packages/app-staking/package.json create mode 100644 packages/app-staking/src/Account.tsx create mode 100644 packages/app-staking/src/StakeList.tsx create mode 100644 packages/app-staking/src/Summary.tsx create mode 100644 packages/app-staking/src/index.css create mode 100644 packages/app-staking/src/index.tsx create mode 100644 packages/app-staking/src/translate.ts create mode 100644 packages/apps/src/routing/staking.ts create mode 100644 packages/ui-keyring/src/account/all.ts create mode 100644 packages/ui-react-rx/src/with/multi.ts diff --git a/README.md b/README.md index 61c98042f8..c594318847 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ The repo is split into a number of packages, each representing an application. T - [app-explorer](packages/app-explorer/) A simple block explorer. It only shows the most recent blocks, updating as they become available. - [app-extrinsics](packages/app-extrinsics/) Submission of extrinsics to a node. - [app-rpc](packages/app-rpc/) Sumission of raw data to RPC endpoints. +- [app-staking](packages/app-staking/) A basic staking management app. - [app-storage](packages/app-storage/) A simple node storage query application. Multiple queries can be queued and updates as new values become available. - [app-toolbox](packages/app-toolbox/) Utilities to manage data. - [app-vanitygen](packages/app-vanitygen/) A toy that allows you to generate vanity addresses. Running `yarn run vanitygen --match ` runs the generator as a Node CLI app. (Orders of a magnitude faster due to the use of libsoldium bindings) diff --git a/jest.config.js b/jest.config.js index 5fc8ee3eea..f65223627a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,7 +2,7 @@ const config = require('@polkadot/dev-react/config/jest'); module.exports = Object.assign({}, config, { moduleNameMapper: { - '@polkadot/app-(accounts|addresses|example|explorer|extrinsics|rpc|storage|toolbox|vanitygen)(.*)$': '/packages/ui-$1/src/$2', + '@polkadot/app-(accounts|addresses|example|explorer|extrinsics|rpc|staking|storage|toolbox|vanitygen)(.*)$': '/packages/ui-$1/src/$2', '@polkadot/ui-(app|identicon|keyring|react-rx|react|signer)(.*)$': '/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' diff --git a/packages/app-accounts/src/Address.tsx b/packages/app-accounts/src/Address.tsx index d2b8531cf1..d0a11e9e7e 100644 --- a/packages/app-accounts/src/Address.tsx +++ b/packages/app-accounts/src/Address.tsx @@ -2,7 +2,7 @@ // This software may be modified and distributed under the terms // of the ISC license. See the LICENSE file for details. -import { BareProps } from '@polkadot/ui-app/types'; +import { I18nProps } from '@polkadot/ui-app/types'; import React from 'react'; @@ -14,7 +14,9 @@ import Nonce from '@polkadot/ui-react-rx/Nonce'; import addressDecode from '@polkadot/util-keyring/address/decode'; import addressEncode from '@polkadot/util-keyring/address/encode'; -type Props = BareProps & { +import translate from './translate'; + +type Props = I18nProps & { value: string; }; @@ -28,7 +30,7 @@ type State = { const DEFAULT_ADDR = '5'.padEnd(16, 'x'); const DEFAULT_SHORT = `${DEFAULT_ADDR.slice(0, 7)}…${DEFAULT_ADDR.slice(-7)}`; -export default class Address extends React.PureComponent { +class Address extends React.PureComponent { state: State = {} as State; static getDerivedStateFromProps ({ value }: Props, { address, publicKey, shortValue }: State): State { @@ -76,6 +78,7 @@ export default class Address extends React.PureComponent { } renderBalance () { + const { t } = this.props; const { isValid, publicKey } = this.state; if (!isValid) { @@ -86,7 +89,9 @@ export default class Address extends React.PureComponent { , { key='nonce' params={publicKey} > - {' transactions'} + {t('address.transactions', { + defaultValue: ' transactions' + })} ]; } } + +export default translate(Address); diff --git a/packages/app-staking/LICENSE b/packages/app-staking/LICENSE new file mode 100644 index 0000000000..d3ba55083d --- /dev/null +++ b/packages/app-staking/LICENSE @@ -0,0 +1,15 @@ +ISC License (ISC) + +Copyright 2017-2018 @polkadot/app-staking authors & contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/packages/app-staking/README.md b/packages/app-staking/README.md new file mode 100644 index 0000000000..8023b83d57 --- /dev/null +++ b/packages/app-staking/README.md @@ -0,0 +1 @@ +# @polkadot/app-staking diff --git a/packages/app-staking/package.json b/packages/app-staking/package.json new file mode 100644 index 0000000000..c3e3bd0a05 --- /dev/null +++ b/packages/app-staking/package.json @@ -0,0 +1,21 @@ +{ + "name": "@polkadot/app-staking", + "version": "0.19.11", + "description": "A basic staking app", + "main": "index.js", + "scripts": {}, + "author": "Jaco Greeff ", + "maintainers": [ + "Jaco Greeff " + ], + "license": "ISC", + "dependencies": { + "@babel/runtime": "^7.0.0-beta.51", + "@polkadot/storage": "^0.26.33", + "@polkadot/ui-app": "^0.19.9", + "@polkadot/ui-keyring": "^0.19.9", + "@polkadot/ui-react": "^0.19.9", + "@polkadot/ui-react-rx": "^0.19.9", + "@polkadot/util-keyring": "^0.26.33" + } +} diff --git a/packages/app-staking/src/Account.tsx b/packages/app-staking/src/Account.tsx new file mode 100644 index 0000000000..74c67d77b8 --- /dev/null +++ b/packages/app-staking/src/Account.tsx @@ -0,0 +1,145 @@ +// Copyright 2017-2018 @polkadot/app-staking authors & contributors +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +import { I18nProps } from '@polkadot/ui-app/types'; +import { Extrinsics } from '@polkadot/extrinsics/types'; +import { SectionItem } from '@polkadot/params/types'; +import { RawParam$Value } from '@polkadot/ui-app/Params/types'; +import { QueueTx$ExtrinsicAdd } from '@polkadot/ui-signer/types'; + +import BN from 'bn.js'; +import React from 'react'; +import extrinsics from '@polkadot/extrinsics'; +import Button from '@polkadot/ui-app/Button'; +import Icon from '@polkadot/ui-app/Icon'; +import classes from '@polkadot/ui-app/util/classes'; +import IdentityIcon from '@polkadot/ui-react/IdentityIcon'; +import RxBalance from '@polkadot/ui-react-rx/Balance'; +import RxNonce from '@polkadot/ui-react-rx/Nonce'; +import decodeAddress from '@polkadot/util-keyring/address/decode'; + +import translate from './translate'; + +type Props = I18nProps & { + address: string, + name: string, + nonce?: BN, + intentionPosition: number, + isIntending: boolean, + isValidator: boolean, + queueExtrinsic: QueueTx$ExtrinsicAdd +}; + +type State = { + nonce: BN; +}; + +class Account extends React.PureComponent { + constructor (props: Props) { + super(props); + + this.state = { + nonce: new BN(0) + }; + } + render () { + const { className, isIntending, style, t } = this.props; + + return ( +
+ {this.renderAccount()} +
+ ); + } + + private renderAccount () { + const { address, isValidator, name, t } = this.props; + const addrShort = `${address.slice(0, 7)}…${address.slice(-7)}`; + + return ( +
+
+ + +
+
{name}
+
{addrShort}
+
+
+ + + {t('account.transactions', { + defaultValue: ' transactions' + })} + +
+ ); + } + + private send (extrinsic: SectionItem, values: Array) { + const { address, queueExtrinsic } = this.props; + const { nonce } = this.state; + const publicKey = decodeAddress(address); + + queueExtrinsic({ + extrinsic, + nonce, + publicKey, + values + }); + } + + private stake = () => { + this.send(extrinsics.staking.public.stake, []); + } + + private unstake = () => { + const { intentionPosition } = this.props; + + this.send(extrinsics.staking.public.unstake, [intentionPosition]); + } + + private onChangeNonce = (nonce: BN) => { + this.setState({ nonce }); + } +} + +export default translate(Account); diff --git a/packages/app-staking/src/StakeList.tsx b/packages/app-staking/src/StakeList.tsx new file mode 100644 index 0000000000..a65aa80330 --- /dev/null +++ b/packages/app-staking/src/StakeList.tsx @@ -0,0 +1,55 @@ +// Copyright 2017-2018 @polkadot/app-staking authors & contributors +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +import { I18nProps } from '@polkadot/ui-app/types'; +import { QueueProps } from '@polkadot/ui-signer/types'; + +import React from 'react'; +import classes from '@polkadot/ui-app/util/classes'; +import keyring from '@polkadot/ui-keyring/index'; +import { QueueConsumer } from '@polkadot/ui-signer/Context'; + +import Account from './Account'; +import translate from './translate'; + +type Props = I18nProps & { + intentions: Array, + validators: Array +}; + +class StakeList extends React.PureComponent { + render () { + const { className, intentions, style, validators } = this.props; + + return ( + + {({ queueExtrinsic }: QueueProps) => ( +
+ {keyring.getAccounts().map((account) => { + const address = account.address(); + const name = account.getMeta().name || ''; + + return ( + + ); + })} +
+ )} +
+ ); + } +} + +export default translate(StakeList); diff --git a/packages/app-staking/src/Summary.tsx b/packages/app-staking/src/Summary.tsx new file mode 100644 index 0000000000..f9c2995557 --- /dev/null +++ b/packages/app-staking/src/Summary.tsx @@ -0,0 +1,194 @@ +// Copyright 2017-2018 @polkadot/app-staking authors & contributors +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +import { Header } from '@polkadot/primitives/header'; +import { I18nProps } from '@polkadot/ui-app/types'; +import { ApiProps } from '@polkadot/ui-react-rx/types'; + +import BN from 'bn.js'; +import React from 'react'; +import apimethods from '@polkadot/jsonrpc'; +import storage from '@polkadot/storage'; +import classes from '@polkadot/ui-app/util/classes'; +import withApiCall from '@polkadot/ui-react-rx/with/apiCall'; +import withMulti from '@polkadot/ui-react-rx/with/multi'; +import withStorage from '@polkadot/ui-react-rx/with/storage'; + +import translate from './translate'; + +type Props = ApiProps & I18nProps & { + intentions: Array, + lastBlockHeader?: Header, + lastSessionChange?: BN, + sessionLength?: BN, + validators: Array +}; + +type StateBalances = { + [index: string]: BN +}; + +type State = { + balances: StateBalances, + subscriptions: Array +}; + +const DEFAULT_BALANCE = new BN(0); +const DEFAULT_BLOCKNUMBER = new BN(0); +const DEFAULT_SESSION_CHANGE = new BN(0); +const DEFAULT_SESSION_LENGTH = new BN(60); + +class Summary extends React.PureComponent { + constructor (props: Props) { + super(props); + + this.state = { + balances: {}, + subscriptions: [] + }; + } + + componentDidUpdate (prevProps: Props) { + const { intentions } = this.props; + + if (intentions !== prevProps.intentions) { + this.subscribeBalances(intentions); + } + } + + private subscribeBalances (accounts: string[]) { + const { api } = this.props; + const { balances, subscriptions } = this.state; + const newBalances = { ...balances }; + + accounts.forEach((account) => { + if (newBalances[account]) { + return; + } else { + newBalances[account] = DEFAULT_BALANCE; + } + + subscriptions.push( + api.state + // Here we pass a parameter to the key generator, so it points to the correct storage entry + .getStorage(storage.staking.public.freeBalanceOf, account) + .subscribe((balance: BN) => { + this.setState(({ balances }: State) => { + const newBalances = { ...balances }; + + newBalances[account] = balance; + + return { + balances: newBalances + } as State; + }); + }) + ); + }); + + this.setState({ + balances: newBalances, + subscriptions + } as State); + } + + componentWillUnmount () { + const { subscriptions } = this.state; + + subscriptions.forEach((sub) => sub.unsubscribe()); + } + + render () { + const { className, intentions, lastBlockHeader, lastSessionChange = DEFAULT_SESSION_CHANGE, style, sessionLength = DEFAULT_SESSION_LENGTH, t, validators } = this.props; + const blockNumber = lastBlockHeader + ? lastBlockHeader.number + : DEFAULT_BLOCKNUMBER; + const intentionHigh = this.calcIntentionsHigh(); + const validatorLow = this.calcValidatorLow(); + + return ( +
+
{t('summary.headline', { + defaultValue: '{{validatorCount}} validators, {{intentionCount}} accounts with intentions', + replace: { + intentionCount: intentions.length, + validatorCount: validators.length + } + })}
+
{t('summary.balance.validator', { + defaultValue: 'lowest validator balance is {{validatorLow}}', + replace: { + validatorLow: validatorLow ? validatorLow.toString() : 'unknown' + } + })}
+
{t('summary.balance.stake', { + defaultValue: ' highest balance intending to stake is {{intentionHigh}}', + replace: { + intentionHigh: intentionHigh ? intentionHigh.toString() : 'unknown' + } + })}
+
{t('summary.countdown', { + defaultValue: 'session block {{remainder}} / {{length}} at #{{blockNumber}}', + replace: { + blockNumber: blockNumber.toString(), + remainder: Math.max(1, blockNumber.sub(lastSessionChange).mod(sessionLength).addn(1).toNumber()).toString(), + length: sessionLength.toString() + } + })}
+
+ ); + } + + private calcIntentionsHigh (): BN | null { + const { intentions, validators } = this.props; + const { balances } = this.state; + + return intentions.reduce((high: BN | null, addr) => { + const balance = validators.includes(addr) + ? null + : balances[addr] || null; + + if (high === null || (balance && high.lt(balance))) { + return balance; + } + + return high; + }, null); + } + + private calcValidatorLow (): BN | null { + const { validators } = this.props; + const { balances } = this.state; + + return validators.reduce((low: BN | null, addr) => { + const balance = balances[addr] || null; + + if (low === null || (balance && low.gt(balance))) { + return balance; + } + + return low; + }, null); + } +} + +export default withMulti( + Summary, + translate, + withApiCall( + apimethods.chain.public.newHead, + { propName: 'lastBlockHeader' } + ), + withStorage( + storage.session.public.length, + { propName: 'sessionLength' } + ), + withStorage( + storage.session.public.lastSessionChange, + { propName: 'lastSessionChange' } + ) +); diff --git a/packages/app-staking/src/index.css b/packages/app-staking/src/index.css new file mode 100644 index 0000000000..fb13db9637 --- /dev/null +++ b/packages/app-staking/src/index.css @@ -0,0 +1,58 @@ +/* Copyright 2017-2018 @polkadot/app-staking authors & contributors +/* This software may be modified and distributed under the terms +/* of the ISC license. See the LICENSE file for details. */ + +.staking--App { + padding: 1em 2rem; +} + +.staking--Summary { + margin-bottom: 2rem; + text-align: right; +} + +.staking--Account { + text-align: center; + margin-bottom: 1rem; +} + +.staking--Account > .button { + margin-top: 1.25rem; + vertical-align: top; +} + +.staking--Account-details { + display: inline-block; + margin-right: 1rem; + text-align: left; +} + +.staking--Account-info { + display: inline-block; +} + +.staking--Account-address { + font-family: monospace; + font-size: 1.5rem; +} + +.staking--Account-icon { + margin-right: 1rem; +} + +.staking--Account-validating { + opacity: 0.25 !important; + padding-top: 8px; + vertical-align: top !important; +} + +.staking--Account-validating.isValidator { + color: red; + opacity: 1 !important; +} + +.staking--Account-balance, +.staking--Account-nonce { + opacity: 0.45; + text-align: right; +} diff --git a/packages/app-staking/src/index.tsx b/packages/app-staking/src/index.tsx new file mode 100644 index 0000000000..e696297145 --- /dev/null +++ b/packages/app-staking/src/index.tsx @@ -0,0 +1,65 @@ +// Copyright 2017-2018 @polkadot/app-staking authors & contributors +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +import { BareProps } from '@polkadot/ui-app/types'; + +import React from 'react'; +import storage from '@polkadot/storage'; +import encodeAddress from '@polkadot/util-keyring/address/encode'; +import classes from '@polkadot/ui-app/util/classes'; +import withStorage from '@polkadot/ui-react-rx/with/storage'; +import withMulti from '@polkadot/ui-react-rx/with/multi'; + +import './index.css'; + +import StakeList from './StakeList'; +import Summary from './Summary'; + +type Props = BareProps & { + intentions?: Array, + validators?: Array +}; + +const transformAddress = (publicKeys: Array) => + publicKeys.map(encodeAddress); + +class App extends React.PureComponent { + render () { + const { className, intentions = [], style, validators = [] } = this.props; + + return ( +
+ + +
+ ); + } +} + +export default withMulti( + App, + withStorage( + storage.staking.public.intentions, + { + propName: 'intentions', + transform: transformAddress + } + ), + withStorage( + storage.session.public.validators, + { + propName: 'validators', + transform: transformAddress + } + ) +); diff --git a/packages/app-staking/src/translate.ts b/packages/app-staking/src/translate.ts new file mode 100644 index 0000000000..00ee500943 --- /dev/null +++ b/packages/app-staking/src/translate.ts @@ -0,0 +1,7 @@ +// Copyright 2017-2018 @polkadot/app-staking authors & contributors +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +import { translate } from 'react-i18next'; + +export default translate(['staking', 'ui']); diff --git a/packages/apps/package.json b/packages/apps/package.json index 45b5c789b8..8a91050dfa 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -23,6 +23,7 @@ "@polkadot/app-explorer": "^0.19.11", "@polkadot/app-extrinsics": "^0.19.11", "@polkadot/app-rpc": "^0.19.11", + "@polkadot/app-staking": "^0.19.11", "@polkadot/app-storage": "^0.19.11", "@polkadot/app-toolbox": "^0.19.11", "@polkadot/app-vanitygen": "^0.19.11", diff --git a/packages/apps/src/routing/index.ts b/packages/apps/src/routing/index.ts index a3a9e58987..2f0db92e64 100644 --- a/packages/apps/src/routing/index.ts +++ b/packages/apps/src/routing/index.ts @@ -12,6 +12,7 @@ import extrinsics from './extrinsics'; // import home from './home'; import rpc from './rpc'; // import settings from './settings'; +import staking from './staking'; import storage from './storage'; import toolbox from './toolbox'; import unknown from './unknown'; @@ -20,7 +21,7 @@ import vanitygen from './vanitygen'; export default ({ default: 'explorer', routes: ([] as Routes).concat( - example, explorer, extrinsics, storage, null, accounts, addresses, vanitygen, null, rpc, toolbox + example, explorer, staking, extrinsics, storage, null, accounts, addresses, vanitygen, null, rpc, toolbox ), unknown } as Routing); diff --git a/packages/apps/src/routing/staking.ts b/packages/apps/src/routing/staking.ts new file mode 100644 index 0000000000..6c70990804 --- /dev/null +++ b/packages/apps/src/routing/staking.ts @@ -0,0 +1,20 @@ +// Copyright 2017-2018 @polkadot/apps authors & contributors +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +import { Routes } from '../types'; + +import Staking from '@polkadot/app-staking/index'; + +export default ([ + { + Component: Staking, + i18n: { + defaultValue: 'Staking' + }, + icon: 'certificate', + isExact: false, + isHidden: false, + name: 'staking' + } +] as Routes); diff --git a/packages/apps/webpack.config.js b/packages/apps/webpack.config.js index 9af0078c24..f319ae76d3 100644 --- a/packages/apps/webpack.config.js +++ b/packages/apps/webpack.config.js @@ -10,7 +10,7 @@ const CopyWebpackPlugin = require('copy-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); -const packages = ['app-accounts', 'app-addresses', 'app-example', 'app-explorer', 'app-extrinsics', 'app-rpc', 'app-storage', 'app-toolbox', 'app-vanitygen', 'ui-app', 'ui-identicon', 'ui-keyring', 'ui-react-rx', 'ui-react', 'ui-signer']; +const packages = ['app-accounts', 'app-addresses', 'app-example', 'app-explorer', 'app-extrinsics', 'app-rpc', 'app-staking', 'app-storage', 'app-toolbox', 'app-vanitygen', 'ui-app', 'ui-identicon', 'ui-keyring', 'ui-react-rx', 'ui-react', 'ui-signer']; function createWebpack ({ alias = {}, context, name = 'index' }) { const pkgJson = require(path.join(context, 'package.json')); diff --git a/packages/ui-keyring/src/account/all.ts b/packages/ui-keyring/src/account/all.ts new file mode 100644 index 0000000000..348b415eff --- /dev/null +++ b/packages/ui-keyring/src/account/all.ts @@ -0,0 +1,16 @@ +// Copyright 2017-2018 @polkadot/ui-keyring authors & contributors +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +import { KeyringAddress, State } from '../types'; + +import get from '../address/get'; + +export default function all (state: State): Array { + return Object + .keys(state.available.account) + .map((address) => + get(state, address, 'account') + ) + .filter((account) => !account.getMeta().isTesting); +} diff --git a/packages/ui-keyring/src/address/get.ts b/packages/ui-keyring/src/address/get.ts index 5360f74788..afa9b1be9d 100644 --- a/packages/ui-keyring/src/address/get.ts +++ b/packages/ui-keyring/src/address/get.ts @@ -8,7 +8,7 @@ import isString from '@polkadot/util/is/string'; import addressDecode from '@polkadot/util-keyring/address/decode'; import addressEncode from '@polkadot/util-keyring/address/encode'; -export default function get (state: State, _address: string | Uint8Array): KeyringAddress { +export default function get (state: State, _address: string | Uint8Array, type: 'account' | 'address' = 'address'): KeyringAddress { const address = isString(_address) ? _address : addressEncode(_address); @@ -18,10 +18,10 @@ export default function get (state: State, _address: string | Uint8Array): Keyri address: (): string => address, isValid: (): boolean => - !!state.available.address[address], + !!state.available[type][address], publicKey: (): Uint8Array => publicKey, getMeta: (): KeyringJson$Meta => - state.available.address[address].meta + state.available[type][address].meta }; } diff --git a/packages/ui-keyring/src/index.ts b/packages/ui-keyring/src/index.ts index d8894cc717..74c6611635 100644 --- a/packages/ui-keyring/src/index.ts +++ b/packages/ui-keyring/src/index.ts @@ -14,6 +14,7 @@ import isAvailable from './isAvailable'; import saveAccount from './account/save'; import saveAccountMeta from './account/meta'; import forgetAddress from './address/forget'; +import getAccounts from './account/all'; import getAddress from './address/get'; import getAddresses from './address/all'; import saveAddress from './address/meta'; @@ -41,6 +42,8 @@ export default ({ forgetAddress(state, address), isAvailable: (address: string | Uint8Array): boolean => isAvailable(state, address), + getAccounts: (): Array => + getAccounts(state), getAddress: (address: string | Uint8Array): KeyringAddress => getAddress(state, address), getAddresses: (): Array => diff --git a/packages/ui-keyring/src/types.d.ts b/packages/ui-keyring/src/types.d.ts index 996f27ae04..4ad6158534 100644 --- a/packages/ui-keyring/src/types.d.ts +++ b/packages/ui-keyring/src/types.d.ts @@ -60,6 +60,7 @@ export type KeyringInstance = { createAccount: (seed: Uint8Array, password?: string, meta?: KeyringPair$Meta) => KeyringPair, forgetAccount: (address: string) => void, forgetAddress: (address: string) => void, + getAccounts: () => Array, getAddress: (address: string | Uint8Array) => KeyringAddress, getAddresses: () => Array, getOptions: (type: KeyringOption$Type) => KeyringOptions, diff --git a/packages/ui-react-rx/src/with/multi.ts b/packages/ui-react-rx/src/with/multi.ts new file mode 100644 index 0000000000..7031f40677 --- /dev/null +++ b/packages/ui-react-rx/src/with/multi.ts @@ -0,0 +1,13 @@ +// Copyright 2017-2018 @polkadot/ui-react-rx authors & contributors +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +import React from 'react'; + +type HOC = (Component: React.ComponentType) => React.ComponentType; + +export default function withMulti (Component: React.ComponentType, ...hocs: Array): React.ComponentType { + return hocs.reduce((Component, hoc) => { + return hoc(Component); + }, Component); +} diff --git a/packages/ui-signer/src/Queue.tsx b/packages/ui-signer/src/Queue.tsx index c10ca4f359..c99fb8c185 100644 --- a/packages/ui-signer/src/Queue.tsx +++ b/packages/ui-signer/src/Queue.tsx @@ -3,13 +3,20 @@ // of the ISC license. See the LICENSE file for details. import { BareProps } from '@polkadot/ui-app/types'; -import { QueueProps, QueueTx, QueueTx$Base, QueueTx$Id, QueueTx$Status } from './types'; +import { ApiProps } from '@polkadot/ui-react-rx/types'; +import { QueueProps, QueueTx, QueueTx$Extrinsic, QueueTx$Base, QueueTx$Id, QueueTx$Status } from './types'; import React from 'react'; +import rpcs from '@polkadot/jsonrpc'; +import withApi from '@polkadot/ui-react-rx/with/api'; +import encode from '@polkadot/extrinsics/codec/encode/extrinsic'; +import isUndefined from '@polkadot/util/is/undefined'; import { QueueProvider } from './Context'; -export type Props = BareProps & { +const rpc = rpcs.author.public.submitExtrinsic; + +export type Props = BareProps & ApiProps & { children: any // node? }; @@ -21,7 +28,7 @@ const defaultState = { let nextId: QueueTx$Id = 0; -export default class Queue extends React.Component { +class Queue extends React.Component { state: State = defaultState; constructor (props: Props) { @@ -30,6 +37,7 @@ export default class Queue extends React.Component { this.state = { queue: [], queueAdd: this.queueAdd, + queueExtrinsic: this.queueExtrinsic, queueSetStatus: this.queueSetStatus }; } @@ -84,4 +92,27 @@ export default class Queue extends React.Component { return id; } + + queueExtrinsic = ({ extrinsic, nonce, publicKey, values }: QueueTx$Extrinsic): QueueTx$Id => { + const { apiSupport } = this.props; + const params = Object.values(extrinsic.params); + const isValid = values.length === params.length && + params.reduce((isValid, param, index) => + isValid && !isUndefined(values[index]), + true + ); + const encoded = isValid && extrinsic.params + ? encode(extrinsic, values, apiSupport) + : new Uint8Array([]); + + return this.queueAdd({ + isValid, + nonce, + publicKey, + rpc, + values: [encoded] + }); + } } + +export default withApi(Queue); diff --git a/packages/ui-signer/src/types.d.ts b/packages/ui-signer/src/types.d.ts index 5c82c118d7..ef1ffbc508 100644 --- a/packages/ui-signer/src/types.d.ts +++ b/packages/ui-signer/src/types.d.ts @@ -3,9 +3,11 @@ // of the ISC license. See the LICENSE file for details. import BN from 'bn.js'; +import { Extrinsics } from '@polkadot/extrinsics/types'; import { SectionItem } from '@polkadot/params/types'; import { Interfaces } from '@polkadot/jsonrpc/types'; import { Param$Values } from '@polkadot/params/types'; +import { RawParam$Value } from '@polkadot/ui-app/Params/types'; export type EncodedMessage = { isValid: boolean, @@ -22,12 +24,20 @@ export type QueueTx$Result = { status: QueueTx$Status } -export type QueueTx$Base = EncodedMessage & { - rpc: SectionItem, +export type AccountInfo = { nonce: BN, publicKey?: Uint8Array | null }; +export type QueueTx$Base = EncodedMessage & AccountInfo & { + rpc: SectionItem +}; + +export type QueueTx$Extrinsic = AccountInfo & { + extrinsic: SectionItem, + values: Array +} + export type QueueTx = QueueTx$Base & { error?: Error, id: QueueTx$Id, @@ -37,11 +47,14 @@ export type QueueTx = QueueTx$Base & { export type QueueTx$MessageAdd = (value: QueueTx$Base) => QueueTx$Id; +export type QueueTx$ExtrinsicAdd = (value: QueueTx$Extrinsic) => QueueTx$Id; + export type QueueTx$MessageSetStatus = (id: number, status: QueueTx$Status, result?: any, error?: Error) => void; export type QueueProps = { queue: Array, queueAdd: QueueTx$MessageAdd, + queueExtrinsic: QueueTx$ExtrinsicAdd, queueSetStatus: QueueTx$MessageSetStatus }; diff --git a/tsconfig.json b/tsconfig.json index 14619ec19b..836286a799 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "@polkadot/app-example/*": [ "packages/app-example/src/*" ], "@polkadot/app-extrinsics/*": [ "packages/app-extrinsics/src/*" ], "@polkadot/app-rpc/*": [ "packages/app-rpc/src/*" ], + "@polkadot/app-staking/*": [ "packages/app-staking/src/*" ], "@polkadot/app-storage/*": [ "packages/app-storage/src/*" ], "@polkadot/app-toolbox/*": [ "packages/app-toolbox/src/*" ], "@polkadot/app-vanitygen/*": [ "packages/app-vanitygen/src/*" ], diff --git a/yarn.lock b/yarn.lock index 4dc414b15a..62785b5fd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -910,7 +910,7 @@ "@polkadot/trie-hash" "^0.26.35" "@polkadot/util" "^0.26.35" -"@polkadot/storage@^0.26.35": +"@polkadot/storage@^0.26.33", "@polkadot/storage@^0.26.35": version "0.26.35" resolved "https://registry.yarnpkg.com/@polkadot/storage/-/storage-0.26.35.tgz#2d4e3aac47612b0319c0c69f369974266d1c1d04" dependencies: @@ -945,7 +945,7 @@ tweetnacl "^1.0.0" xxhashjs "^0.2.2" -"@polkadot/util-keyring@^0.26.35": +"@polkadot/util-keyring@^0.26.33", "@polkadot/util-keyring@^0.26.35": version "0.26.35" resolved "https://registry.yarnpkg.com/@polkadot/util-keyring/-/util-keyring-0.26.35.tgz#266002c6591665cf8a7df6c5ad3ed840f8b400a5" dependencies: