Nomination target dashboard (#1953)

* Nomination target dashboard

* Bump API
This commit is contained in:
Jaco Greeff 2019-11-27 15:11:00 +01:00 committed by GitHub
parent fb88bd7b3b
commit fba7778c74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 439 additions and 51 deletions

View File

@ -10,10 +10,10 @@
"packages/*"
],
"resolutions": {
"@polkadot/api": "^0.97.0-beta.33",
"@polkadot/api-contract": "^0.97.0-beta.33",
"@polkadot/api": "^0.97.0-beta.34",
"@polkadot/api-contract": "^0.97.0-beta.34",
"@polkadot/keyring": "^1.7.0-beta.8",
"@polkadot/types": "^0.97.0-beta.33",
"@polkadot/types": "^0.97.0-beta.34",
"@polkadot/util": "^1.7.0-beta.8",
"@polkadot/util-crypto": "^1.7.0-beta.8",
"babel-core": "^7.0.0-bridge.0",

View File

@ -11,7 +11,7 @@
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.7.4",
"@polkadot/api-contract": "^0.97.0-beta.33",
"@polkadot/api-contract": "^0.97.0-beta.34",
"@polkadot/react-components": "^0.37.0-beta.128"
}
}

View File

@ -0,0 +1,79 @@
// Copyright 2017-2019 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { Balance } from '@polkadot/types/interfaces';
import { I18nProps } from '@polkadot/react-components/types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import { SummaryBox, CardSummary } from '@polkadot/react-components';
import { useApi, trackStream } from '@polkadot/react-hooks';
import { formatBalance } from '@polkadot/util';
import translate from '../translate';
interface Props extends I18nProps {
totalStaked: BN;
}
interface StakeInfo {
percentage: string;
staked: string | null;
total: string | null;
}
function Summary ({ t, totalStaked }: Props): React.ReactElement<Props> {
const { api } = useApi();
const totalInsurance = trackStream<Balance>(api.query.balances.totalIssuance, []);
const [{ percentage, staked, total }, setStakeInfo] = useState<StakeInfo>({ percentage: '-', staked: null, total: null });
useEffect((): void => {
if (totalInsurance && totalStaked?.gtn(0)) {
setStakeInfo({
percentage: `${(totalStaked.muln(10000).div(totalInsurance).toNumber() / 100).toFixed(2)}%`,
staked: totalStaked.toString(),
total: totalInsurance.toString()
});
}
}, [totalInsurance, totalStaked]);
return (
<SummaryBox>
<section>
<CardSummary
className='ui--media-small'
label={t('total staked')}
>
{
staked
? `${formatBalance(staked, false)}${formatBalance.calcSi(staked).value}`
: '-'
}
</CardSummary>
<CardSummary
className='ui--media-small'
label=''
>/</CardSummary>
<CardSummary
className='ui--media-small'
label={t('total issuance')}
>
{
total
? `${formatBalance(total, false)}${formatBalance.calcSi(total).value}`
: '-'
}
</CardSummary>
</section>
<CardSummary
className='ui--media-small'
label={t('staked')}
>
{percentage}
</CardSummary>
</SummaryBox>
);
}
export default translate(Summary);

View File

@ -0,0 +1,54 @@
// Copyright 2017-2019 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.v
import { I18nProps } from '@polkadot/react-components/types';
import { ValidatorInfo } from './types';
import React from 'react';
import { AddressMini, Icon } from '@polkadot/react-components';
import { FormatBalance } from '@polkadot/react-query';
import { formatNumber } from '@polkadot/util';
import translate from '../translate';
interface Props extends I18nProps {
info: ValidatorInfo;
toggleFavorite: (accountId: string) => void;
}
function Validator ({ info: { accountId, bondOther, bondOwn, bondTotal, commission, isFavorite, key, numNominators, rankOverall, rewardPayout }, toggleFavorite }: Props): React.ReactElement<Props> {
const _onFavorite = (): void => toggleFavorite(key);
const _onQueryStats = (): void => {
window.location.hash = `/staking/query/${key}`;
};
return (
<tr>
<td>
<Icon
className={`favorite ${isFavorite && 'isSelected'}`}
name={isFavorite ? 'star' : 'star outline'}
onClick={_onFavorite}
/>
</td>
<td className='number'>{formatNumber(rankOverall)}</td>
<td className='address'><AddressMini value={accountId} /></td>
<td className='number'><FormatBalance value={commission} /></td>
<td className='number'>{formatNumber(numNominators)}</td>
<td className='number'><FormatBalance value={bondTotal} /></td>
<td className='number'><FormatBalance value={bondOwn} /></td>
<td className='number'><FormatBalance value={bondOther} /></td>
<td className='number'><FormatBalance value={rewardPayout} /></td>
<td>
<Icon
className='staking--stats'
name='line graph'
onClick={_onQueryStats}
/>
</td>
</tr>
);
}
export default translate(Validator);

View File

@ -0,0 +1,224 @@
// Copyright 2017-2019 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DerivedStakingElected } from '@polkadot/api-derive/types';
import { I18nProps } from '@polkadot/react-components/types';
import { ComponentProps } from '../types';
import { ValidatorInfo } from './types';
import BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { InputBalance, Icon, Tooltip } from '@polkadot/react-components';
import { useApi, useFavorites, trackStream } from '@polkadot/react-hooks';
import { STORE_FAVS_BASE } from '../constants';
import translate from '../translate';
import Summary from './Summary';
import Validator from './Validator';
interface Props extends I18nProps, ComponentProps {
}
interface AllInfo {
totalStaked: BN;
validators: ValidatorInfo[];
}
function sortValidators (list: ValidatorInfo[]): ValidatorInfo[] {
return list
.sort((a, b): number => b.bondTotal.cmp(a.bondTotal))
.map((info, index): ValidatorInfo => {
info.rankBonded = index + 1;
return info;
})
.sort((a, b): number => b.commission.cmp(a.commission))
.map((info, index): ValidatorInfo => {
info.rankCommission = index + 1;
return info;
})
.sort((a, b): number => a.rewardSplit.cmp(b.rewardSplit))
.map((info, index): ValidatorInfo => {
info.rankReward = index + 1;
return info;
})
.sort((a, b): number =>
a.rankReward === b.rankReward
? a.rankCommission === b.rankCommission
? a.rankBonded === b.rankBonded
? 0
: a.rankBonded < b.rankBonded
? 1
: -1
: a.rankCommission < b.rankCommission
? 1
: -1
: a.rankReward < b.rankReward
? 1
: -1
)
.map((info, index): ValidatorInfo => {
info.rankOverall = index + 1;
return info;
})
.sort((a, b): number =>
a.isFavorite === b.isFavorite
? 0
: a.isFavorite
? -1
: 1
);
}
function Targets ({ className, sessionRewards, t }: Props): React.ReactElement<Props> {
const { api } = useApi();
const [amount, setAmount] = useState<BN | undefined>(new BN(1000));
const electedInfo = trackStream<DerivedStakingElected>(api.derive.staking.electedInfo, []);
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS_BASE);
const [{ validators, totalStaked }, setWorkable] = useState<AllInfo>({ totalStaked: new BN(0), validators: [] });
useEffect((): void => {
if (electedInfo) {
let totalStaked = new BN(0);
const numValidators = electedInfo.info.length;
const lastReward = sessionRewards[sessionRewards.length - 1]
? sessionRewards[sessionRewards.length - 1].reward.divn(numValidators)
: new BN(0);
const validators = sortValidators(
electedInfo.info.map(({ accountId, stakers, validatorPrefs }): ValidatorInfo => {
const exposure = stakers || {
total: api.createType('Compact<Balance>'),
own: api.createType('Compact<Balance>'),
others: api.createType('Vec<IndividualExposure>')
};
const prefs = validatorPrefs || {
validatorPayment: api.createType('Compact<Balance>')
};
const bondOwn = exposure.own.unwrap();
const bondTotal = exposure.total.unwrap();
const commission = prefs.validatorPayment.unwrap();
const key = accountId.toString();
const rewardSplit = lastReward.sub(commission);
const calcAmount = amount || new BN(0);
const rewardPayout = rewardSplit.gtn(0)
? calcAmount.mul(rewardSplit).div(calcAmount.add(bondTotal))
: new BN(0);
totalStaked = totalStaked.add(bondTotal);
return {
accountId,
bondOther: bondTotal.sub(bondOwn),
bondOwn,
bondShare: 0,
bondTotal,
isFavorite: favorites.includes(key),
key,
commission,
numNominators: exposure.others.length,
rankBonded: 0,
rankCommission: 0,
rankOverall: 0,
rankReward: 0,
rewardPayout,
rewardSplit
};
})
);
setWorkable({ totalStaked, validators });
}
}, [amount, electedInfo, favorites, sessionRewards]);
return (
<div className={className}>
<Summary totalStaked={totalStaked} />
<InputBalance
help={t('The amount that will be used on a per-validator basis to calculate rewards for that validator.')}
label={t('amount to use for estimation')}
onChange={setAmount}
value={amount}
/>
<table>
<thead>
<tr className='header'>
<th>&nbsp;</th>
<th>
<Icon
name='info circle'
data-tip
data-for='ranking-trigger'
/>
<Tooltip
text={t('Ranking is done of the estimated best return, taking the commission and total bonded amount into account. It does not incorporate validator liveliness according to length of operation nor number of blocks produced.')}
trigger='ranking-trigger'
/>
</th>
<th>validator</th>
<th className='number'>commission</th>
<th className='number'>nominators</th>
<th className='number'>total stake</th>
<th className='number'>own stake</th>
<th className='number'>other stake</th>
<th className='number'>payout (est.)</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{validators.map((info): React.ReactNode =>
<Validator
info={info}
key={info.key}
toggleFavorite={toggleFavorite}
/>
)}
</tbody>
</table>
</div>
);
}
export default translate(
styled(Targets)`
text-align: center;
table {
margin: 1.5rem auto;
tr {
&:nth-child(even), &.header {
background: #f2f2f2;
}
td, th {
padding: 0.25rem 0.5rem;
text-align: left;
&.number {
text-align: right;
}
}
th {
color: rgba(0,0,0,.6);
font-family: sans-serif;
font-weight: 100;
font-size: 1.25rem;
}
}
}
i.icon {
cursor: pointer;
}
.favorite.isSelected {
color: darkorange;
}
`
);

View File

@ -0,0 +1,25 @@
// Copyright 2017-2019 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountId, Balance } from '@polkadot/types/interfaces';
import BN from 'bn.js';
export interface ValidatorInfo {
accountId: AccountId;
bondOther: BN;
bondOwn: Balance;
bondShare: number;
bondTotal: Balance;
commission: Balance;
isFavorite: boolean;
key: string;
numNominators: number;
rankBonded: number;
rankCommission: number;
rankOverall: number;
rankReward: number;
rewardPayout: BN;
rewardSplit: BN;
}

View File

@ -16,10 +16,11 @@ import { HelpOverlay } from '@polkadot/react-components';
import Tabs from '@polkadot/react-components/Tabs';
import { trackStream, useAccounts, useApi } from '@polkadot/react-hooks';
import Actions from './Actions';
import basicMd from './md/basic.md';
import Actions from './Actions';
import Overview from './Overview';
import Query from './Query';
import Targets from './Targets';
import { MAX_SESSIONS } from './constants';
import translate from './translate';
import useSessionRewards from './useSessionRewards';
@ -89,6 +90,10 @@ function App ({ basePath, className, t }: Props): React.ReactElement<Props> {
name: 'actions',
text: t('Account actions')
},
{
name: 'returns',
text: t('Returns')
},
{
hasParams: true,
name: 'query',
@ -101,6 +106,7 @@ function App ({ basePath, className, t }: Props): React.ReactElement<Props> {
<Route path={`${basePath}/actions`}>{_renderComponent(Actions)}</Route>
<Route path={`${basePath}/query/:value`}>{_renderComponent(Query)}</Route>
<Route path={`${basePath}/query`}>{_renderComponent(Query)}</Route>
<Route path={`${basePath}/returns`}>{_renderComponent(Targets)}</Route>
</Switch>
{_renderComponent(Overview, routeMatch?.isExact ? '' : 'staking--hidden')}
</main>

View File

@ -31,7 +31,7 @@
"homepage": "https://github.com/polkadot-js/ui/tree/master/packages/ui-reactive#readme",
"dependencies": {
"@babel/runtime": "^7.7.4",
"@polkadot/api": "^0.97.0-beta.33",
"@polkadot/api": "^0.97.0-beta.34",
"@polkadot/extension-dapp": "^0.14.0-beta.6",
"edgeware-node-types": "^1.0.10",
"rxjs-compat": "^6.5.3"

View File

@ -2029,35 +2029,35 @@
dependencies:
"@types/node" ">= 8"
"@polkadot/api-contract@^0.97.0-beta.33":
version "0.97.0-beta.33"
resolved "https://registry.yarnpkg.com/@polkadot/api-contract/-/api-contract-0.97.0-beta.33.tgz#5a1952ddfdb0690aba19cda6b2730d4e26a17d57"
integrity sha512-Q5XDZnHYu+ABXyBPjKgc7I9IXGSMBZQWdvyAF3OVr0kJimQUYJwUQP5j9DQi2cWFi7Pg9P1aNXfEPp89aABN7g==
"@polkadot/api-contract@^0.97.0-beta.34":
version "0.97.0-beta.34"
resolved "https://registry.yarnpkg.com/@polkadot/api-contract/-/api-contract-0.97.0-beta.34.tgz#4d4a38c8447468c50d7731b9c644e8582f3cbb7c"
integrity sha512-S33bKfLNZ8LGXQmhu9/cQLa630HhnsqZaht++Blg4FgwSGPzY6CN4OYPFlazM7z58tLDOFXrWtSSHzypREDmww==
dependencies:
"@babel/runtime" "^7.7.4"
"@polkadot/types" "^0.97.0-beta.33"
"@polkadot/types" "^0.97.0-beta.34"
"@polkadot/api-derive@^0.97.0-beta.33":
version "0.97.0-beta.33"
resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-0.97.0-beta.33.tgz#19f6b94adb677c09e27f3a066517189059f88254"
integrity sha512-jVzBrdp8o3My3yhU7+ip7KI06SL0uYAY7VlvbYmIy3iPkttm0cbp1HD9sZhqbntQo15w8TT6KkgE16tm8xHr+w==
"@polkadot/api-derive@^0.97.0-beta.34":
version "0.97.0-beta.34"
resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-0.97.0-beta.34.tgz#5cac3de8d6670e76483987e8cd66e985a0c69a17"
integrity sha512-NMrEI2aj6jtK5J83rZ8eO65J4L1O8S2cUgYWOI7Cio1rCK3hm05S+2Yi376fO0LfKt5nDgnCLUBl9ADHhHT4Pg==
dependencies:
"@babel/runtime" "^7.7.4"
"@polkadot/api" "^0.97.0-beta.33"
"@polkadot/types" "^0.97.0-beta.33"
"@polkadot/api" "^0.97.0-beta.34"
"@polkadot/types" "^0.97.0-beta.34"
"@polkadot/api@^0.97.0-beta.33":
version "0.97.0-beta.33"
resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-0.97.0-beta.33.tgz#0edc5cf5047fcfa018f3c1b75b73a5cacb515f65"
integrity sha512-FMOprhm7sNUiX1hepFZftoBm+8DgBg4IByqhKfNLOhAXPpYvU7owwGxQIyZ9EXCUsT8LxMxhCArh4TcE/pKQGQ==
"@polkadot/api@^0.97.0-beta.34":
version "0.97.0-beta.34"
resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-0.97.0-beta.34.tgz#846d34726450d6796ce44c1a56bfc1ff267657c0"
integrity sha512-B4vT8/bjV046LHOdeJFLrCDmEagfhyUF4/JvOk18mTM9wYg9fUEfkVcFVTMgrDj0QpXBsNSEv+W1goizbXtxog==
dependencies:
"@babel/runtime" "^7.7.4"
"@polkadot/api-derive" "^0.97.0-beta.33"
"@polkadot/api-derive" "^0.97.0-beta.34"
"@polkadot/keyring" "^1.7.0-beta.8"
"@polkadot/metadata" "^0.97.0-beta.33"
"@polkadot/rpc-core" "^0.97.0-beta.33"
"@polkadot/rpc-provider" "^0.97.0-beta.33"
"@polkadot/types" "^0.97.0-beta.33"
"@polkadot/metadata" "^0.97.0-beta.34"
"@polkadot/rpc-core" "^0.97.0-beta.34"
"@polkadot/rpc-provider" "^0.97.0-beta.34"
"@polkadot/types" "^0.97.0-beta.34"
"@polkadot/util-crypto" "^1.7.0-beta.8"
"@polkadot/dev-react@^0.32.0-beta.15":
@ -2161,10 +2161,10 @@
dependencies:
"@babel/runtime" "^7.7.4"
"@polkadot/jsonrpc@^0.97.0-beta.33":
version "0.97.0-beta.33"
resolved "https://registry.yarnpkg.com/@polkadot/jsonrpc/-/jsonrpc-0.97.0-beta.33.tgz#0ad64eeb395767dc0a0c226e68d5bf92771011fe"
integrity sha512-1kd+1YOqqaHhIMbGCCtYrAkEUblHERlZPQ7+eJy5qZIxYQPuCknMzG6McnUAtEqjhiEpfnlfxDRTVyOWlIEvFg==
"@polkadot/jsonrpc@^0.97.0-beta.34":
version "0.97.0-beta.34"
resolved "https://registry.yarnpkg.com/@polkadot/jsonrpc/-/jsonrpc-0.97.0-beta.34.tgz#b4e58b2c689d1dd115e62e95423850204f0814db"
integrity sha512-kF/R+XeGfY4iI+EE2HRjm6exj35r8l/Zf7q5Y7sUuh54cNKJ6ILVUtIT5Io2LjimvJ3Csf8d5GOUSraNOHnO4A==
dependencies:
"@babel/runtime" "^7.7.4"
@ -2177,13 +2177,13 @@
"@polkadot/util" "^1.7.0-beta.8"
"@polkadot/util-crypto" "^1.7.0-beta.8"
"@polkadot/metadata@^0.97.0-beta.33":
version "0.97.0-beta.33"
resolved "https://registry.yarnpkg.com/@polkadot/metadata/-/metadata-0.97.0-beta.33.tgz#53fd7f447aead53f226013c8cce6e4f3a6823b75"
integrity sha512-JRqbcsMEDso6vUa/mHS1Ri7kocbeKcyjHIBwW/QO1nr7aLLmTCv5RVq2xnFtbzGsvEg0sN0QVbbaNzp1MJ7INQ==
"@polkadot/metadata@^0.97.0-beta.34":
version "0.97.0-beta.34"
resolved "https://registry.yarnpkg.com/@polkadot/metadata/-/metadata-0.97.0-beta.34.tgz#db74e4547fd88bd1ebebd487b43d245f37b5fd96"
integrity sha512-B9go5UcIMEANsKlcHYSXYDZsgFLLQ82izqdS+njqc6V2F4bCMQsUmGa8T7miCa4W6EkOb+HqtPLooXpa3+pA/w==
dependencies:
"@babel/runtime" "^7.7.4"
"@polkadot/types" "^0.97.0-beta.33"
"@polkadot/types" "^0.97.0-beta.34"
"@polkadot/util" "^1.7.0-beta.8"
"@polkadot/util-crypto" "^1.7.0-beta.8"
@ -2212,25 +2212,25 @@
qrcode-generator "^1.4.4"
react-qr-reader "^2.2.1"
"@polkadot/rpc-core@^0.97.0-beta.33":
version "0.97.0-beta.33"
resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-0.97.0-beta.33.tgz#fa7bbf4cc65ca49ab8fc1f7eb1c360b935825b9f"
integrity sha512-HuU4yqKLUjiPFSxzx8TrMoX0OTbfch3sLfw9cFJ9Yqbei2shf7j8ndLjwAH5oUPEdN/qHqdsLjQ3m54lt5Qcfg==
"@polkadot/rpc-core@^0.97.0-beta.34":
version "0.97.0-beta.34"
resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-0.97.0-beta.34.tgz#acb73b711f7a6286e2b610908cc6df2d162e08bd"
integrity sha512-qm9R83DuTktT1jDsNfY/yD6AFIrbpOPV3mdF7ZltDxY89OD1fmkM//8xY3gUV4lwrKu9oManVYtKTBlHwHVDTQ==
dependencies:
"@babel/runtime" "^7.7.4"
"@polkadot/jsonrpc" "^0.97.0-beta.33"
"@polkadot/rpc-provider" "^0.97.0-beta.33"
"@polkadot/types" "^0.97.0-beta.33"
"@polkadot/jsonrpc" "^0.97.0-beta.34"
"@polkadot/rpc-provider" "^0.97.0-beta.34"
"@polkadot/types" "^0.97.0-beta.34"
"@polkadot/util" "^1.7.0-beta.8"
rxjs "^6.5.3"
"@polkadot/rpc-provider@^0.97.0-beta.33":
version "0.97.0-beta.33"
resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-0.97.0-beta.33.tgz#af4055e0734e9349e2d33aa3b94a293eea553166"
integrity sha512-dbDa0rhWXEbi+/on8u/fl94tZxyt79wnNevxF+T8sAKrD5avZmKbcXOizsBml1Ax8y/EJjl34xPJpBMDx1uB/g==
"@polkadot/rpc-provider@^0.97.0-beta.34":
version "0.97.0-beta.34"
resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-0.97.0-beta.34.tgz#28d612d0086cba553522d97b3a632630a3f5bf42"
integrity sha512-QEtqcGMf70DeFq6wKOB529NIV2+3fnJej97LfShHj5Us/LpQ/WXscCIuYxMnA2T3GaltFtzxYxYWLWg/anf7yA==
dependencies:
"@babel/runtime" "^7.7.4"
"@polkadot/metadata" "^0.97.0-beta.33"
"@polkadot/metadata" "^0.97.0-beta.34"
"@polkadot/util" "^1.7.0-beta.8"
"@polkadot/util-crypto" "^1.7.0-beta.8"
eventemitter3 "^4.0.0"
@ -2244,10 +2244,10 @@
dependencies:
"@types/chrome" "^0.0.91"
"@polkadot/types@^0.97.0-beta.33":
version "0.97.0-beta.33"
resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-0.97.0-beta.33.tgz#d83d46eabc7b6d5c496f21792bff42dc736eefc9"
integrity sha512-MWHxn9pdh7BCR7tZOPp90jnRdhh1T4GP6sctYYDpskTldneMB+URP38rUoCJuH0c/zJ5zl0z1RUiFksU8Qy7Gg==
"@polkadot/types@^0.97.0-beta.34":
version "0.97.0-beta.34"
resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-0.97.0-beta.34.tgz#6cae02b4170a1712eb3e7ad7be7e3326b675b947"
integrity sha512-ET/nFYfbXmYkNbVW1++3dEg+Ci56gfT2a+LZbiMgrjDF/08BEztBagD6F1A2UceY2+NfITtSeJjK6HhOrWF12Q==
dependencies:
"@babel/runtime" "^7.7.4"
"@polkadot/util" "^1.7.0-beta.8"