(un)Nominate functionality (#183)

* (un)Nominate functionality

* Small cleanups
This commit is contained in:
Jaco Greeff 2018-08-03 18:55:44 +02:00 committed by GitHub
parent b78d1296e9
commit bf5f75d491
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 281 additions and 44 deletions

View File

@ -11,6 +11,7 @@
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.0.0-beta.51",
"@polkadot/app-extrinsics": "^0.19.15",
"@polkadot/storage": "^0.26.36",
"@polkadot/ui-app": "^0.19.15",
"@polkadot/ui-keyring": "^0.19.15",

View File

@ -11,28 +11,40 @@ import { QueueTx$ExtrinsicAdd } from '@polkadot/ui-signer/types';
import BN from 'bn.js';
import React from 'react';
import extrinsics from '@polkadot/extrinsics';
import storage from '@polkadot/storage';
import Button from '@polkadot/ui-app/Button';
import Icon from '@polkadot/ui-app/Icon';
import Input from '@polkadot/ui-app/Input';
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 withMulti from '@polkadot/ui-react-rx/with/multi';
import withStorage from '@polkadot/ui-react-rx/with/storage';
import decodeAddress from '@polkadot/util-keyring/address/decode';
import encodeAddress from '@polkadot/util-keyring/address/encode';
import UnnominateButton from './UnnominateButton';
import translate from './translate';
type Props = I18nProps & {
address: string,
name: string,
nonce?: BN,
nominating?: string,
nominatorsFor?: Array<string>,
intentionPosition: number,
isIntending: boolean,
isValidator: boolean,
queueExtrinsic: QueueTx$ExtrinsicAdd
queueExtrinsic: QueueTx$ExtrinsicAdd,
validators: Array<string>
};
type State = {
nonce: BN;
isInNominate: boolean,
isNomineeValid: boolean,
nominee: string,
nonce: BN
};
class Account extends React.PureComponent<Props, State> {
@ -40,11 +52,15 @@ class Account extends React.PureComponent<Props, State> {
super(props);
this.state = {
nonce: new BN(0)
isInNominate: false,
isNomineeValid: false,
nonce: new BN(0),
nominee: ''
};
}
render () {
const { className, isIntending, style, t } = this.props;
const { className, style } = this.props;
return (
<div
@ -52,29 +68,16 @@ class Account extends React.PureComponent<Props, State> {
style={style}
>
{this.renderAccount()}
<Button
isDisabled={isIntending}
isPrimary
onClick={this.stake}
text={t('account.stake', {
defaultValue: 'stake'
})}
/>
<Button
isDisabled={!isIntending}
isNegative
onClick={this.unstake}
text={t('account.unstake', {
defaultValue: 'unstake'
})}
/>
{this.renderButtons()}
{this.renderNominate()}
</div>
);
}
private renderAccount () {
const { address, isValidator, name, t } = this.props;
const addrShort = `${address.slice(0, 7)}…${address.slice(-7)}`;
// TODO: Still now sure what the best layout is, so just keeping the shortened around
// const addrShort = `${address.slice(0, 7)}…${address.slice(-7)}`;
return (
<div className='staking--Account-details'>
@ -91,7 +94,7 @@ class Account extends React.PureComponent<Props, State> {
/>
<div className='staking--Account-info'>
<div className='staking--Account-name'>{name}</div>
<div className='staking--Account-address'>{addrShort}</div>
<div className='staking--Account-address'>{address}</div>
</div>
</div>
<RxBalance
@ -114,6 +117,116 @@ class Account extends React.PureComponent<Props, State> {
);
}
private renderButtons () {
const { isInNominate } = this.state;
if (isInNominate) {
return null;
}
const { address, isIntending, nominating, t } = this.props;
const isNominating = !!nominating;
const canStake = !isIntending && !isNominating;
if (canStake) {
return (
<div className='staking--Account-buttons'>
<Button.Group>
<Button
isPrimary
onClick={this.stake}
text={t('account.stake', {
defaultValue: 'stake'
})}
/>
<Button.Or />
<Button
isPrimary
onClick={this.toggleNominate}
text={t('account.nominate', {
defaultValue: 'nominate'
})}
/>
</Button.Group>
</div>
);
}
if (isNominating) {
return (
<div className='staking--Account-buttons'>
<Button.Group>
<UnnominateButton
address={address || ''}
nominating={nominating || ''}
onClick={this.unnominate}
/>
</Button.Group>
</div>
);
}
return (
<div className='staking--Account-buttons'>
<Button.Group>
<Button
isNegative
onClick={this.unstake}
text={t('account.unstake', {
defaultValue: 'unstake'
})}
/>
</Button.Group>
</div>
);
}
private renderNominate () {
const { isInNominate, isNomineeValid, nominee } = this.state;
if (!isInNominate) {
return null;
}
const { t } = this.props;
return (
<div className='staking--Account-nominate'>
<div className='ui--row'>
<Input
className='medium'
isError={!isNomineeValid}
label={t('nominator.address', {
defaultValue: 'nominate the following validator'
})}
onChange={this.onChangeNominee}
value={nominee}
/>
</div>
<div className='ui--row'>
<div className='medium'>
<Button.Group>
<Button
onClick={this.toggleNominate}
text={t('nominators.discard', {
defaultValue: 'Cancel'
})}
/>
<Button
isDisabled={!isNomineeValid}
isPrimary
onClick={this.nominate}
text={t('nominator.nominate', {
defaultValue: 'nominate'
})}
/>
</Button.Group>
</div>
</div>
</div>
);
}
private send (extrinsic: SectionItem<Extrinsics>, values: Array<RawParam$Value>) {
const { address, queueExtrinsic } = this.props;
const { nonce } = this.state;
@ -127,6 +240,35 @@ class Account extends React.PureComponent<Props, State> {
});
}
private toggleNominate = () => {
this.setState(
({ isInNominate }: State) => ({
isInNominate: !isInNominate
})
);
}
private onChangeNominee = (nominee: string) => {
const { validators } = this.props;
this.setState({
isNomineeValid: validators.includes(nominee),
nominee
});
}
private nominate = () => {
const { nominee } = this.state;
this.send(extrinsics.staking.public.nominate, [nominee]);
this.toggleNominate();
}
private unnominate = (index: number) => {
this.send(extrinsics.staking.public.unnominate, [index]);
}
private stake = () => {
this.send(extrinsics.staking.public.stake, []);
}
@ -142,4 +284,24 @@ class Account extends React.PureComponent<Props, State> {
}
}
export default translate(Account);
export default withMulti(
Account,
translate,
withStorage(
storage.staking.public.nominatorsFor,
{
propName: 'nominatorsFor',
paramProp: 'address',
transform: (publicKeys: Array<Uint8Array>) =>
publicKeys.map(encodeAddress)
}
),
withStorage(
storage.staking.public.nominating,
{
propName: 'nominating',
paramProp: 'address',
transform: encodeAddress
}
)
);

View File

@ -42,6 +42,7 @@ class StakeList extends React.PureComponent<Props> {
key={address}
name={name}
queueExtrinsic={queueExtrinsic}
validators={validators}
/>
);
})}

View File

@ -0,0 +1,64 @@
// 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 React from 'react';
import storage from '@polkadot/storage';
import Button from '@polkadot/ui-app/Button';
import classes from '@polkadot/ui-app/util/classes';
import withMulti from '@polkadot/ui-react-rx/with/multi';
import withStorage from '@polkadot/ui-react-rx/with/storage';
import encodeAddress from '@polkadot/util-keyring/address/encode';
import translate from './translate';
type Props = I18nProps & {
address: string,
nominating: string,
nominatorsFor: Array<string>,
onClick: (index: number) => void
};
class UnnominateButton extends React.Component<Props> {
render () {
const { className, nominating, nominatorsFor, style, t } = this.props;
if (!nominating || !nominatorsFor || nominatorsFor.length === 0) {
return null;
}
return (
<Button
className={classes('staking--Account-buttons', className)}
style={style}
isNegative
onClick={this.onClick}
text={t('account.unnominate', {
defaultValue: 'unnominate'
})}
/>
);
}
onClick = () => {
const { address, nominatorsFor, onClick } = this.props;
onClick(nominatorsFor.indexOf(address));
}
}
export default withMulti(
UnnominateButton,
translate,
withStorage(
storage.staking.public.nominatorsFor,
{
propName: 'nominatorsFor',
paramProp: 'nominating',
transform: (publicKeys: Array<Uint8Array>) =>
publicKeys.map(encodeAddress)
}
)
);

View File

@ -12,19 +12,29 @@
}
.staking--Account {
text-align: center;
margin-bottom: 1rem;
}
.staking--Account > .button {
.staking--Account .ui--Button-Group {
display: inline-block;
margin-top: 1.25rem;
margin-left: 1em;
vertical-align: top;
}
.staking--Account .ui--Button-Group {
text-align: left;
}
.staking--Account-buttons,
.staking--Account-details {
display: inline-block;
margin-right: 1rem;
text-align: left;
vertical-align: top;
}
.staking--Account-nominate {
padding: 0 0 3em 3em;
}
.staking--Account-info {

View File

@ -21,7 +21,7 @@ type Props = BareProps & {
validators?: Array<string>
};
const transformAddress = (publicKeys: Array<Uint8Array>) =>
const transformAddresses = (publicKeys: Array<Uint8Array>) =>
publicKeys.map(encodeAddress);
class App extends React.PureComponent<Props> {
@ -52,14 +52,14 @@ export default withMulti(
storage.staking.public.intentions,
{
propName: 'intentions',
transform: transformAddress
transform: transformAddresses
}
),
withStorage(
storage.session.public.validators,
{
propName: 'validators',
transform: transformAddress
transform: transformAddresses
}
)
);

View File

@ -27,16 +27,6 @@
border-color: #e0b4b4;
}
.ui--Button-Group {
.ui.buttons {
.ui.button.compact.mini.basic + .button:nth-child(2) {
border-top-right-radius: $small-corner;
border-bottom-right-radius: $small-corner;
}
.ui.button.compact.mini.basic + .button:nth-child(2),
.ui.button.compact.mini.basic + .button:nth-child(4) {
border-top-left-radius: $small-corner;
border-bottom-left-radius: $small-corner;
}
}
.ui--Button-Group .button {
border-radius: .28571429rem !important;
}

View File

@ -77,3 +77,11 @@
left: -7px;
opacity: 1;
}
.ui.button:disabled,
.ui.buttons .disabled.button,
.ui.disabled.active.button,
.ui.disabled.button,
.ui.disabled.button:hover {
opacity: 0.1 !important;
}

View File

@ -27,7 +27,7 @@ type State<T> = RxProps<T> & {
// FIXME proper types for attributes
export default function withStorage<T> (key: SectionItem<Storages>, { onChange, params, propName = 'value', transform }: StorageOptions<T> = {}): HOC<T> {
export default function withStorage<T> (key: SectionItem<Storages>, { onChange, params, paramProp = 'params', propName = 'value', transform }: StorageOptions<T> = {}): HOC<T> {
const keyCreator = storageKey(key);
const storageTransform = createTransform(key);
const createKey = (propParams?: Array<Storage$Key$Value>): Uint8Array => {
@ -59,7 +59,7 @@ export default function withStorage<T> (key: SectionItem<Storages>, { onChange,
}
async componentDidUpdate (prevProps: any) {
if (!isEqual(this.props.params, prevProps.params)) {
if (!isEqual(this.props[paramProp], prevProps[paramProp])) {
await this.triggerUpdate();
}
}
@ -99,7 +99,7 @@ export default function withStorage<T> (key: SectionItem<Storages>, { onChange,
);
};
const key = createKey(this.props.params);
const key = createKey(this.props[paramProp]);
let value;
try {

View File

@ -25,6 +25,7 @@ export type StorageTransform = (input: any, index: number) => Param$Values | nul
export type StorageOptions<T> = Options<T> & {
params?: Array<Storage$Key$Value | undefined>;
paramProp?: string,
};
export type HOC<T> = (Component: React.ComponentType<any>, defaultProps?: DefaultProps<T>) => React.ComponentType<any>;