(un)Nominate functionality (#183)
* (un)Nominate functionality * Small cleanups
This commit is contained in:
parent
b78d1296e9
commit
bf5f75d491
@ -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",
|
||||
|
||||
@ -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
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@ -42,6 +42,7 @@ class StakeList extends React.PureComponent<Props> {
|
||||
key={address}
|
||||
name={name}
|
||||
queueExtrinsic={queueExtrinsic}
|
||||
validators={validators}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
64
packages/app-staking/src/UnnominateButton.tsx
Normal file
64
packages/app-staking/src/UnnominateButton.tsx
Normal 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)
|
||||
}
|
||||
)
|
||||
);
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
1
packages/ui-react-rx/src/with/types.d.ts
vendored
1
packages/ui-react-rx/src/with/types.d.ts
vendored
@ -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>;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user