Expand the example, indicating validators with sorting (#129)

This commit is contained in:
Jaco Greeff 2018-07-10 10:32:00 +02:00 committed by GitHub
parent 378fed132c
commit c03a4473f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 509 additions and 3 deletions

View File

@ -9,6 +9,8 @@ This guide will take you through some basic examples in using the Components and
- [Query storage via API](tut-003.md) Shows how to construct an API query to pull information from storage
- [Query storage, replace HOC](tut-004.md) Queries storage, replacing the previous use of our HOC
- [Combining proposal and intentions](tut-005.md) Using the information collected previously, combine the displays
- [Showing selected validators](tut-006.md) Expand on intentions, filtering with current validators
- [Sorting intentions by balance](tut-007.md) Cleanup our display, sorting the intentions by balance
## ... and in the end ...

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 382 KiB

View File

@ -47,3 +47,11 @@ class Comp extends React.PureComponent<ApiProps, State> {
# walk-through
The render is straight-forward. For each of our intentions, render a row in a table showing the icon & address, the balance of the account and the number of proposals.
# actual output
![tut-005](https://raw.githubusercontent.com/polkadot-js/apps/master/examples/tut-005.png)
# next steps
[Next up](tut-006.md) we will expand our list, showing the actual activive validators.

BIN
examples/tut-005.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

45
examples/tut-006.md Normal file
View File

@ -0,0 +1,45 @@
# adding validators
[Previously](tut-005.md), the list of accounts wishing to paritipate in staking has been shown. The next step would be to merge this with the list of validators (as selected from the intentions) and show these slightly differently.
# component
Building on the component created previously, we can expand [the selection](../packages/app-example/src/comp-006.tsx) (and rendering) to query the active validators.
```js
class Comp extends React.PureComponent<ApiProps, State> {
...
subscribeValidators () {
const { api } = this.props;
const key = createStorageKey(validatorsMethod)();
const transform = storageTransform(validatorsMethod);
return api.state
.getStorage(key)
.subscribe((value) => {
this.setState({
validators: (transform(value, 0) as any[]).map(encodeAddress)
});
});
}
...
renderAccount = (address: string, proposals: number[] = [], isValidator: boolean = false) => {
return (
<tr className={isValidator ? 'validator' : ''} key={address}>
...
);
}
}
```
# walk through
Building on what we have done previously, we now also subscribe to the list of validators (`storage.session.public.validators`) and uses this to highlight the actual active validators that we have found. It is important to note that the validator list is a subset of intentions, i.e. validators will continue to be shows as active intentions while they are part of the network.
# actual output
![tut-006](https://raw.githubusercontent.com/polkadot-js/apps/master/examples/tut-006.png)
# next steps
For the [next round](tut-007.md), we will cleanup the display somewhat, allowing us to make sense of the actual unordered information displayed.

BIN
examples/tut-006.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

70
examples/tut-007.md Normal file
View File

@ -0,0 +1,70 @@
# sorting by balance
After [the last section](tut-006.md), we are at a point we we have the intentions, actual validators and their balances. For users, we would like this display to be more usable, so here we are aiming on sorting by balance.
# component
Expanding on our previous component, we now change the previously used `<Balance />` component, into doing [actual `freeBalanceOf` queries](../packages/app-example/src/comp-007.tsx). There are 2 options here - we could also use the `onChange` event on the `Balance` component to return the display value. However here we opt for querying the raw value, transforming it and sorting with it.
```js
class Comp extends React.PureComponent<ApiProps, State> {
...
subscribeBalances (accounts: string[]) {
const { api } = this.props;
const keyCreator = createStorageKey(balanceMethod);
const { balances, subscriptions } = this.state;
const newBalances = { ...balances };
accounts.forEach((account) => {
if (newBalances[account]) {
return;
} else {
newBalances[account] = ZERO;
}
subscriptions.push(
api.state
// Here we pass a parameter to the key generator, so it points to the correct storage entry
.getStorage(keyCreator(account))
.subscribe((balance) => {
this.setState(({ balances }: State) => {
const newBalances = { ...balances };
newBalances[account] = u8aToBn(balance, true);
return {
balances: newBalances
};
});
})
);
});
this.setState({
balances: newBalances,
subscriptions
});
}
...
render () {
const { balances, intentions, proposals, validators } = this.state;
const sortedIntentions = intentions.sort((a, b) =>
(balances[b] || ZERO).cmp(balances[a] || ZERO)
);
...
}
...
}
```
# walk through
By now the subscription approach is known. Where things do differ however is the passing of a parameter to the key encoder. Since each balance entry is denoted by the actual account, the hashing needs to take the actual account and append it to the prefix location before making a hash that the runtime understands.
For the actual storage value, we have additional formatting on the raw value. Here we are taking the `Uint8Array` input and transforming it to a `BN`. All values in the storage are stored as little-endian-encoded values, hence passing the additional `isLe` flag to the `Uint8Array` -> `BN` conversion helper.
NOTE: It would make sense for future versions of the API to be expanded so additional formatting, either here or e.g. accountId are not needed. (At this early stage however the APIs are still at a too-low level to transparently take care of the transformation - although all information is contained in the params of the key being encoded)
# actual output
![tut-007](https://raw.githubusercontent.com/polkadot-js/apps/master/examples/tut-007.png)

BIN
examples/tut-007.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

View File

@ -12,6 +12,7 @@
"dependencies": {
"@babel/runtime": "^7.0.0-beta.51",
"@polkadot/storage": "^0.26.9",
"@polkadot/ui-react": "^0.18.10",
"@polkadot/ui-react-rx": "^0.18.10",
"@polkadot/util-keyring": "^0.26.9"
}

View File

@ -94,7 +94,7 @@ class Comp extends React.PureComponent<ApiProps, State> {
const { intentions, proposals } = this.state;
return (
<table>
<table className='accounts'>
<thead>
<tr>
<th />

View File

@ -0,0 +1,145 @@
// Copyright 2017-2018 @polkadot/app-example authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { ApiProps } from '@polkadot/ui-react-rx/types';
import React from 'react';
import storage from '@polkadot/storage';
import createStorageKey from '@polkadot/storage/key';
import withApi from '@polkadot/ui-react-rx/with/api';
import storageTransform from '@polkadot/ui-react-rx/with/transform/storage';
import encodeAddress from '@polkadot/util-keyring/address/encode';
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
import Balance from '@polkadot/ui-react-rx/Balance';
const intentionsMethod = storage.staking.public.intentions;
const proposalMethod = storage.democracy.public.proposals;
const validatorsMethod = storage.session.public.validators;
type State = {
intentions: string[],
proposals: {
[index: string]: number[]
},
subscriptions: any[],
validators: string[]
};
class Comp extends React.PureComponent<ApiProps, State> {
constructor (props: ApiProps) {
super(props);
this.state = {
intentions: [],
proposals: {},
subscriptions: [],
validators: []
};
}
componentDidMount () {
this.setState({
subscriptions: [
this.subscribeIntentions(),
this.subscribeProposals(),
this.subscribeValidators()
]
});
}
subscribeIntentions () {
const { api } = this.props;
const key = createStorageKey(intentionsMethod)();
const transform = storageTransform(intentionsMethod);
return api.state
.getStorage(key)
.subscribe((value) => {
this.setState({
intentions: (transform(value, 0) as any[]).map(encodeAddress)
});
});
}
subscribeProposals () {
const { api } = this.props;
const key = createStorageKey(proposalMethod)();
const transform = storageTransform(proposalMethod);
return api.state
.getStorage(key)
.subscribe((value) => {
this.setState({
proposals: (transform(value, 0) as any[])
.reduce((proposals, [propIdx, proposal, accountId]) => {
const address = encodeAddress(accountId);
if (!proposals[address]) {
proposals[address] = [propIdx];
} else {
proposals[address].push(propIdx);
}
return proposals;
}, {})
});
});
}
subscribeValidators () {
const { api } = this.props;
const key = createStorageKey(validatorsMethod)();
const transform = storageTransform(validatorsMethod);
return api.state
.getStorage(key)
.subscribe((value) => {
this.setState({
validators: (transform(value, 0) as any[]).map(encodeAddress)
});
});
}
componentWillUnmount () {
const { subscriptions } = this.state;
subscriptions.forEach((sub) => sub.unsubscribe());
}
render () {
const { intentions, proposals, validators } = this.state;
return (
<table className='accounts'>
<thead>
<tr>
<th />
<th>Address</th>
<th>Balance</th>
<th>Proposals</th>
</tr>
</thead>
<tbody>
{intentions.map((address) => (
this.renderAccount(address, proposals[address], validators.includes(address))
))}
</tbody>
</table>
);
}
renderAccount = (address: string, proposals: number[] = [], isValidator: boolean = false) => {
return (
<tr className={isValidator ? 'validator' : ''} key={address}>
<td><IdentityIcon size={24} value={address} /></td>
<td>{address}</td>
<td><Balance params={address} /></td>
<td>{proposals.length}</td>
</tr>
);
}
}
export default withApi(Comp);

View File

@ -0,0 +1,195 @@
// Copyright 2017-2018 @polkadot/app-example authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { ApiProps } from '@polkadot/ui-react-rx/types';
import BN from 'bn.js';
import React from 'react';
import storage from '@polkadot/storage';
import createStorageKey from '@polkadot/storage/key';
import IdentityIcon from '@polkadot/ui-react/IdentityIcon';
import withApi from '@polkadot/ui-react-rx/with/api';
import storageTransform from '@polkadot/ui-react-rx/with/transform/storage';
import encodeAddress from '@polkadot/util-keyring/address/encode';
import u8aToBn from '@polkadot/util/u8a/toBn';
const balanceMethod = storage.staking.public.freeBalanceOf;
const intentionsMethod = storage.staking.public.intentions;
const proposalMethod = storage.democracy.public.proposals;
const validatorsMethod = storage.session.public.validators;
const ZERO = new BN(0);
type State = {
balances: {
[index: string]: BN
},
intentions: string[],
proposals: {
[index: string]: number[]
},
subscriptions: any[],
validators: string[]
};
class Comp extends React.PureComponent<ApiProps, State> {
constructor (props: ApiProps) {
super(props);
this.state = {
balances: {},
intentions: [],
proposals: {},
subscriptions: [],
validators: []
};
}
componentDidMount () {
this.setState({
subscriptions: [
this.subscribeIntentions(),
this.subscribeProposals(),
this.subscribeValidators()
]
});
}
subscribeIntentions () {
const { api } = this.props;
const key = createStorageKey(intentionsMethod)();
const transform = storageTransform(intentionsMethod);
return api.state
.getStorage(key)
.subscribe((value) => {
const intentions = (transform(value, 0) as any[]).map(encodeAddress);
this.setState({ intentions }, () => {
this.subscribeBalances(intentions);
});
});
}
subscribeProposals () {
const { api } = this.props;
const key = createStorageKey(proposalMethod)();
const transform = storageTransform(proposalMethod);
return api.state
.getStorage(key)
.subscribe((value) => {
this.setState({
proposals: (transform(value, 0) as any[])
.reduce((proposals, [propIdx, proposal, accountId]) => {
const address = encodeAddress(accountId);
if (!proposals[address]) {
proposals[address] = [propIdx];
} else {
proposals[address].push(propIdx);
}
return proposals;
}, {})
});
});
}
subscribeValidators () {
const { api } = this.props;
const key = createStorageKey(validatorsMethod)();
const transform = storageTransform(validatorsMethod);
return api.state
.getStorage(key)
.subscribe((value) => {
const validators = (transform(value, 0) as any[]).map(encodeAddress);
this.setState({ validators });
});
}
subscribeBalances (accounts: string[]) {
const { api } = this.props;
const keyCreator = createStorageKey(balanceMethod);
const { balances, subscriptions } = this.state;
const newBalances = { ...balances };
accounts.forEach((account) => {
if (newBalances[account]) {
return;
} else {
newBalances[account] = ZERO;
}
subscriptions.push(
api.state
// Here we pass a parameter to the key generator, so it points to the correct storage entry
.getStorage(keyCreator(account))
.subscribe((balance) => {
this.setState(({ balances }: State) => {
const newBalances = { ...balances };
newBalances[account] = u8aToBn(balance, true);
return {
balances: newBalances
};
});
})
);
});
this.setState({
balances: newBalances,
subscriptions
});
}
componentWillUnmount () {
const { subscriptions } = this.state;
subscriptions.forEach((sub) => sub.unsubscribe());
}
render () {
const { balances, intentions, proposals, validators } = this.state;
const sortedIntentions = intentions.sort((a, b) =>
(balances[b] || ZERO).cmp(balances[a] || ZERO)
);
return (
<table className='accounts'>
<thead>
<tr>
<th />
<th>Address</th>
<th>Balance</th>
<th>Proposals</th>
</tr>
</thead>
<tbody>
{sortedIntentions.map((address) => (
this.renderAccount(address, balances[address], proposals[address], validators.includes(address))
))}
</tbody>
</table>
);
}
renderAccount = (address: string, balance: BN = ZERO, proposals: number[] = [], isValidator: boolean = false) => {
return (
<tr className={isValidator ? 'validator' : ''} key={address}>
<td><IdentityIcon size={24} value={address} /></td>
<td>{address}</td>
<td>{balance.toString(10)}</td>
<td>{proposals.length}</td>
</tr>
);
}
}
export default withApi(Comp);

View File

@ -0,0 +1,17 @@
// Copyright 2017-2018 @polkadot/app-example 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';
import Comp from './comp-006';
export default class App extends React.PureComponent<any> {
render () {
return (
<div className='example--App'>
<Comp />
</div>
);
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2017-2018 @polkadot/app-example 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';
import Comp from './comp-007';
export default class App extends React.PureComponent<any> {
render () {
return (
<div className='example--App'>
<Comp />
</div>
);
}
}

View File

@ -15,6 +15,10 @@
margin-right: 0.5em;
}
.example--App td {
.example--App table.accounts td {
padding: 0.125em 0.5em;
}
.example--App table.accounts tr.validator {
background: #f2f2f2;
}

View File

@ -10,9 +10,11 @@ import Tut002 from './comp-002';
import Tut003 from './comp-003';
import Tut004 from './comp-004';
import Tut005 from './comp-005';
import Tut006 from './comp-006';
import Tut007 from './comp-007';
const Components: { [index: string]: React.ComponentType<any> } = {
Tut002, Tut003, Tut004, Tut005
Tut002, Tut003, Tut004, Tut005, Tut006, Tut007
};
type State = {