Adds delegations overview and actions (#3122)
* add tab and query * display accounts amount and conviction * delegation modal without react-component * add conviction dropdown * number not string * use string rather than accountId and KeyringAddress * all changing * use ConvictionDropdown from react-components * delegate column in accounts * with expander * with modals * ordering * Apply suggestions from code review Co-authored-by: Jaco Greeff <jacogr@gmail.com> * ordering * implement suggestion and fix lint * Small layout adjustments with help text * Only delegate to owned accounts * Use AddressMini for delegation layout * Remove ununsed imports Co-authored-by: Jaco Greeff <jacogr@gmail.com>
This commit is contained in:
parent
e1510d4209
commit
e76ac8395b
@ -7,6 +7,7 @@ import { DeriveBalancesAll, DeriveDemocracyLock } from '@polkadot/api-derive/typ
|
||||
import { ActionStatus } from '@polkadot/react-components/Status/types';
|
||||
import { RecoveryConfig } from '@polkadot/types/interfaces';
|
||||
import { KeyringAddress } from '@polkadot/ui-keyring/types';
|
||||
import { Delegation } from '../types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useCallback, useContext, useState, useEffect } from 'react';
|
||||
@ -15,6 +16,7 @@ import { ApiPromise } from '@polkadot/api';
|
||||
import { getLedger } from '@polkadot/react-api';
|
||||
import { AddressInfo, AddressMini, AddressSmall, Badge, Button, ChainLock, CryptoType, Forget, Icon, IdentityIcon, LinkExternal, Menu, Popup, StatusContext, Tags } from '@polkadot/react-components';
|
||||
import { useAccountInfo, useApi, useCall, useToggle } from '@polkadot/react-hooks';
|
||||
import { FormatBalance } from '@polkadot/react-query';
|
||||
import { Option } from '@polkadot/types';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
import { BN_ZERO, formatBalance, formatNumber } from '@polkadot/util';
|
||||
@ -23,6 +25,7 @@ import { useTranslation } from '../translate';
|
||||
import { createMenuGroup } from '../util';
|
||||
import Backup from './modals/Backup';
|
||||
import ChangePass from './modals/ChangePass';
|
||||
import DelegateModal from './modals/Delegate';
|
||||
import Derive from './modals/Derive';
|
||||
import IdentityMain from './modals/IdentityMain';
|
||||
import IdentitySub from './modals/IdentitySub';
|
||||
@ -30,12 +33,14 @@ import MultisigApprove from './modals/MultisigApprove';
|
||||
import RecoverAccount from './modals/RecoverAccount';
|
||||
import RecoverSetup from './modals/RecoverSetup';
|
||||
import Transfer from './modals/Transfer';
|
||||
import UndelegateModal from './modals/Undelegate';
|
||||
import useMultisigApprovals from './useMultisigApprovals';
|
||||
import useProxies from './useProxies';
|
||||
|
||||
interface Props {
|
||||
account: KeyringAddress;
|
||||
className?: string;
|
||||
delegation?: Delegation;
|
||||
filter: string;
|
||||
isFavorite: boolean;
|
||||
setBalance: (address: string, value: BN) => void;
|
||||
@ -67,7 +72,7 @@ function createClearDemocracyTx (api: ApiPromise, address: string, unlockableIds
|
||||
);
|
||||
}
|
||||
|
||||
function Account ({ account: { address, meta }, className = '', filter, isFavorite, setBalance, toggleFavorite }: Props): React.ReactElement<Props> | null {
|
||||
function Account ({ account: { address, meta }, className = '', delegation, filter, isFavorite, setBalance, toggleFavorite }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { queueExtrinsic } = useContext(StatusContext);
|
||||
const api = useApi();
|
||||
@ -94,6 +99,8 @@ function Account ({ account: { address, meta }, className = '', filter, isFavori
|
||||
const [isRecoverSetupOpen, toggleRecoverSetup] = useToggle();
|
||||
const [isSettingsOpen, toggleSettings] = useToggle();
|
||||
const [isTransferOpen, toggleTransfer] = useToggle();
|
||||
const [isDelegateOpen, toggleDelegate] = useToggle();
|
||||
const [isUndelegateOpen, toggleUndelegate] = useToggle();
|
||||
|
||||
useEffect((): void => {
|
||||
if (balancesAll) {
|
||||
@ -340,6 +347,36 @@ function Account ({ account: { address, meta }, className = '', filter, isFavori
|
||||
<AddressMini value={meta.parentAddress} />
|
||||
)}
|
||||
</td>
|
||||
<td className='address ui--media-1500'>
|
||||
{isDelegateOpen && (
|
||||
<DelegateModal
|
||||
amount={delegation?.amount}
|
||||
conviction={delegation?.conviction}
|
||||
delegatedAccount={delegation?.accountDelegated}
|
||||
delegatingAccount={address}
|
||||
key='modal-delegate'
|
||||
onClose={toggleDelegate}
|
||||
/>
|
||||
)}
|
||||
{isUndelegateOpen && (
|
||||
<UndelegateModal
|
||||
accountDelegating={address}
|
||||
key='modal-delegate'
|
||||
onClose={toggleUndelegate}
|
||||
/>
|
||||
)}
|
||||
{delegation && (
|
||||
<AddressMini
|
||||
summary={
|
||||
<div>
|
||||
<FormatBalance value={delegation.amount} />
|
||||
<div>{delegation.conviction.toString()}</div>
|
||||
</div>
|
||||
}
|
||||
value={delegation.accountDelegated}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='number'>
|
||||
<CryptoType accountId={address} />
|
||||
</td>
|
||||
@ -484,6 +521,28 @@ function Account ({ account: { address, meta }, className = '', filter, isFavori
|
||||
{t('Multisig approvals')}
|
||||
</Menu.Item>
|
||||
])}
|
||||
{delegation?.accountDelegated && createMenuGroup([
|
||||
(<Menu.Item
|
||||
key='changeDelegate'
|
||||
onClick={toggleDelegate}
|
||||
>
|
||||
{t('Change democracy delegation')}
|
||||
</Menu.Item>),
|
||||
(<Menu.Item
|
||||
key='undelegate'
|
||||
onClick={toggleUndelegate}
|
||||
>
|
||||
{t('Undelegate')}
|
||||
</Menu.Item>)
|
||||
])}
|
||||
{!delegation?.accountDelegated && createMenuGroup([
|
||||
(<Menu.Item
|
||||
key='delegate'
|
||||
onClick={toggleDelegate}
|
||||
>
|
||||
{t('Delegate democracy votes')}
|
||||
</Menu.Item>)
|
||||
])}
|
||||
<ChainLock
|
||||
className='accounts--network-toggle'
|
||||
genesisHash={genesisHash}
|
||||
|
||||
@ -3,15 +3,15 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { ActionStatus } from '@polkadot/react-components/Status/types';
|
||||
import { KeyringAddress } from '@polkadot/ui-keyring/types';
|
||||
import { SortedAccount } from './types';
|
||||
import { Voting } from '@polkadot/types/interfaces';
|
||||
import { Delegation, SortedAccount } from '../types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
import { getLedger, isLedger } from '@polkadot/react-api';
|
||||
import { useApi, useAccounts, useFavorites, useIpfs, useToggle } from '@polkadot/react-hooks';
|
||||
import { useApi, useAccounts, useCall, useFavorites, useIpfs, useToggle } from '@polkadot/react-hooks';
|
||||
import { FormatBalance } from '@polkadot/react-query';
|
||||
import { Button, Input, Table } from '@polkadot/react-components';
|
||||
import { BN_ZERO } from '@polkadot/util';
|
||||
@ -25,12 +25,18 @@ import Qr from './modals/Qr';
|
||||
import Account from './Account';
|
||||
import BannerClaims from './BannerClaims';
|
||||
import BannerExtension from './BannerExtension';
|
||||
import { sortAccounts } from '../util';
|
||||
|
||||
interface Balances {
|
||||
accounts: Record<string, BN>;
|
||||
balanceTotal?: BN;
|
||||
}
|
||||
|
||||
interface Sorted {
|
||||
sortedAccounts: SortedAccount[];
|
||||
sortedAddresses: string[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
onStatusChange: (status: ActionStatus) => void;
|
||||
@ -51,53 +57,6 @@ async function queryLedger (): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function expandList (mapped: SortedAccount[], entry: SortedAccount): SortedAccount[] {
|
||||
mapped.push(entry);
|
||||
|
||||
entry.children.forEach((entry): void => {
|
||||
expandList(mapped, entry);
|
||||
});
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
function sortAccounts (addresses: string[], favorites: string[]): SortedAccount[] {
|
||||
const mapped = addresses
|
||||
.map((address) => keyring.getAccount(address))
|
||||
.filter((account): account is KeyringAddress => !!account)
|
||||
.map((account): SortedAccount => ({
|
||||
account,
|
||||
children: [],
|
||||
isFavorite: favorites.includes(account.address)
|
||||
}))
|
||||
.sort((a, b) => (a.account.meta.whenCreated || 0) - (b.account.meta.whenCreated || 0));
|
||||
|
||||
return mapped
|
||||
.filter((entry): boolean => {
|
||||
const parentAddress = entry.account.meta.parentAddress;
|
||||
|
||||
if (parentAddress) {
|
||||
const parent = mapped.find(({ account: { address } }) => address === parentAddress);
|
||||
|
||||
if (parent) {
|
||||
parent.children.push(entry);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.reduce(expandList, [])
|
||||
.sort((a, b): number =>
|
||||
a.isFavorite === b.isFavorite
|
||||
? 0
|
||||
: b.isFavorite
|
||||
? 1
|
||||
: -1
|
||||
);
|
||||
}
|
||||
|
||||
function Overview ({ className = '', onStatusChange }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
@ -110,15 +69,45 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
|
||||
const [isQrOpen, toggleQr] = useToggle();
|
||||
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS);
|
||||
const [{ balanceTotal }, setBalances] = useState<Balances>({ accounts: {} });
|
||||
const [sortedAccounts, setSortedAccounts] = useState<SortedAccount[]>([]);
|
||||
const [filterOn, setFilter] = useState<string>('');
|
||||
const [sortedAccountsWithDelegation, setSortedAccountsWithDelegation] = useState<SortedAccount[]>([]);
|
||||
const [{ sortedAccounts, sortedAddresses }, setSorted] = useState<Sorted>({ sortedAccounts: [], sortedAddresses: [] });
|
||||
const delegations = useCall<Voting[]>(api.query.democracy.votingOf.multi, [sortedAddresses]);
|
||||
|
||||
useEffect((): void => {
|
||||
setSortedAccounts(
|
||||
sortAccounts(allAccounts, favorites)
|
||||
);
|
||||
const sortedAccounts = sortAccounts(allAccounts, favorites);
|
||||
const sortedAddresses = sortedAccounts.map((a) => a.account.address);
|
||||
|
||||
setSorted({ sortedAccounts, sortedAddresses });
|
||||
}, [allAccounts, favorites]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!delegations?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedAccountsWithDelegation = sortedAccounts?.map((account, index) => {
|
||||
let delegation: Delegation | undefined;
|
||||
|
||||
if (delegations[index].isDelegating) {
|
||||
const _delegation = delegations[index].asDelegating;
|
||||
|
||||
delegation = {
|
||||
accountDelegated: _delegation.target.toString(),
|
||||
amount: _delegation.balance,
|
||||
conviction: _delegation.conviction
|
||||
};
|
||||
}
|
||||
|
||||
return ({
|
||||
...account,
|
||||
delegation
|
||||
});
|
||||
});
|
||||
|
||||
setSortedAccountsWithDelegation(sortedAccountsWithDelegation);
|
||||
}, [delegations, sortedAccounts]);
|
||||
|
||||
const _setBalance = useCallback(
|
||||
(account: string, balance: BN) =>
|
||||
setBalances(({ accounts }: Balances): Balances => {
|
||||
@ -135,6 +124,7 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
|
||||
const header = useMemo(() => [
|
||||
[t('accounts'), 'start', 3],
|
||||
[t('parent'), 'address'],
|
||||
[t('delegation'), 'address ui--media-1500'],
|
||||
[t('type')],
|
||||
[t('tags'), 'start'],
|
||||
[t('transactions'), 'ui--media-1500'],
|
||||
@ -245,9 +235,10 @@ function Overview ({ className = '', onStatusChange }: Props): React.ReactElemen
|
||||
footer={footer}
|
||||
header={header}
|
||||
>
|
||||
{sortedAccounts.map(({ account, isFavorite }): React.ReactNode => (
|
||||
{sortedAccountsWithDelegation.map(({ account, delegation, isFavorite }): React.ReactNode => (
|
||||
<Account
|
||||
account={account}
|
||||
delegation={delegation}
|
||||
filter={filterOn}
|
||||
isFavorite={isFavorite}
|
||||
key={account.address}
|
||||
|
||||
110
packages/page-accounts/src/Accounts/modals/Delegate.tsx
Normal file
110
packages/page-accounts/src/Accounts/modals/Delegate.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
// Copyright 2017-2020 @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 { AmountValidateState } from '../types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useState } from 'react';
|
||||
import { ConvictionDropdown, InputAddress, InputBalance, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { BalanceFree } from '@polkadot/react-query';
|
||||
import { Conviction } from '@polkadot/types/interfaces';
|
||||
import { BN_ZERO } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../../translate';
|
||||
import ValidateAmount from './InputValidateAmount';
|
||||
|
||||
interface Props {
|
||||
amount?: BN;
|
||||
conviction?: Conviction;
|
||||
delegatedAccount?: string;
|
||||
delegatingAccount?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function Delegate ({ amount: _amount, conviction: _conviction, delegatedAccount: _delegatedAccount, delegatingAccount: _delegatingAccount, onClose }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [amountError, setAmountError] = useState<AmountValidateState | null>(null);
|
||||
const [maxBalance] = useState<BN | undefined>();
|
||||
const [amount, setAmount] = useState<BN | undefined>(_amount);
|
||||
const [delegatingAccount, setDelegatingAccount] = useState<string | null>(_delegatingAccount || null);
|
||||
const [delegatedAccount, setDelegatedAccount] = useState<string | null>(_delegatedAccount || null);
|
||||
const [conviction, setConviction] = useState(_conviction?.toNumber() || 1);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className='staking--Delegate'
|
||||
header= {t<string>('Delegate democracy vote')}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content className='ui--signer-Signer-Content'>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
label={t<string>('delegating account')}
|
||||
onChange={setDelegatingAccount}
|
||||
type='account'
|
||||
value={delegatingAccount}
|
||||
/>
|
||||
<InputAddress
|
||||
label={t<string>('delegated account')}
|
||||
onChange={setDelegatedAccount}
|
||||
type='account'
|
||||
value={delegatedAccount}
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('Any democracy vote performed by the delegated account will result in an additional vote from the delegating account')}</p>
|
||||
<p>{t<string>('If the delegated account is currently voting in a referendum, the delegating vote and conviction will be added.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputBalance
|
||||
help={t<string>('Amount to delegate for any democracy vote. This is adjusted using the available funds on the account.')}
|
||||
isError={!!amountError?.error}
|
||||
label={t<string>('delegating amount')}
|
||||
labelExtra={
|
||||
<BalanceFree
|
||||
label={<span className='label'>{t<string>('balance')}</span>}
|
||||
params={delegatingAccount}
|
||||
/>
|
||||
}
|
||||
maxValue={maxBalance}
|
||||
onChange={setAmount}
|
||||
value={amount}
|
||||
/>
|
||||
<ValidateAmount
|
||||
amount={amount}
|
||||
delegatingAccount={delegatingAccount}
|
||||
onError={setAmountError}
|
||||
/>
|
||||
<ConvictionDropdown
|
||||
help={t<string>('The conviction that will be used for each delegated vote.')}
|
||||
label={t<string>('conviction')}
|
||||
onChange={setConviction}
|
||||
value={conviction}
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t('The amount to allocate and the conviction that will be applied to all votes made on a referendum.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={onClose}>
|
||||
<TxButton
|
||||
accountId={delegatingAccount}
|
||||
icon='sign-in-alt'
|
||||
isDisabled={!amount?.gt(BN_ZERO) || !!amountError?.error}
|
||||
isPrimary
|
||||
label={t<string>('Delegate')}
|
||||
onStart={onClose}
|
||||
params={[delegatedAccount, conviction, amount]}
|
||||
tx='democracy.delegate'
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Delegate);
|
||||
@ -0,0 +1,58 @@
|
||||
// Copyright 2017-2020 @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 { DeriveBalancesAll } from '@polkadot/api-derive/types';
|
||||
import { AmountValidateState } from '../types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Icon } from '@polkadot/react-components';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { BN_ZERO } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../../translate';
|
||||
|
||||
interface Props {
|
||||
amount?: BN | null;
|
||||
delegatingAccount: string | null;
|
||||
onError: (state: AmountValidateState | null) => void;
|
||||
}
|
||||
|
||||
function ValidateAmount ({ amount, delegatingAccount, onError }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const delegatingAccountBalance = useCall<DeriveBalancesAll>(api.derive.balances.all, [delegatingAccount]);
|
||||
const [{ error, warning }, setResult] = useState<AmountValidateState>({ error: null, warning: null });
|
||||
|
||||
useEffect((): void => {
|
||||
if (delegatingAccountBalance?.freeBalance && amount?.gt(BN_ZERO)) {
|
||||
let newError: string | null = null;
|
||||
|
||||
if (amount.gte(delegatingAccountBalance.freeBalance)) {
|
||||
newError = t('The maximum amount you can delegate is the amount of funds available on the delegating account.');
|
||||
}
|
||||
|
||||
setResult((state): AmountValidateState => {
|
||||
const error = state.error !== newError ? newError : state.error;
|
||||
const warning = state.warning;
|
||||
|
||||
onError((error || warning) ? { error, warning } : null);
|
||||
|
||||
return { error, warning };
|
||||
});
|
||||
}
|
||||
}, [api, onError, amount, t, delegatingAccountBalance]);
|
||||
|
||||
if (error || warning) {
|
||||
return (
|
||||
<article className={error ? 'error' : 'warning'}>
|
||||
<div><Icon icon='exclamation-triangle' />{error || warning}</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default React.memo(ValidateAmount);
|
||||
53
packages/page-accounts/src/Accounts/modals/Undelegate.tsx
Normal file
53
packages/page-accounts/src/Accounts/modals/Undelegate.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright 2017-2020 @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 React from 'react';
|
||||
import { InputAddress, Modal, TxButton } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../../translate';
|
||||
|
||||
interface Props {
|
||||
accountDelegating: string | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function Undelegate ({ accountDelegating, onClose }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className='staking--Undelegate'
|
||||
header= {t<string>('Undelegate')}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content className='ui--signer-Signer-Content'>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
defaultValue={accountDelegating}
|
||||
isDisabled
|
||||
label={t<string>('delegating account')}
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t<string>('You will remove any delegation made by this acccount')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={onClose}>
|
||||
<TxButton
|
||||
accountId={accountDelegating}
|
||||
icon='sign-in-alt'
|
||||
isPrimary
|
||||
label={t<string>('Undelegate')}
|
||||
onStart={onClose}
|
||||
params={[]}
|
||||
tx='democracy.undelegate'
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Undelegate);
|
||||
@ -2,10 +2,31 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { ActionStatus } from '@polkadot/react-components/Status/types';
|
||||
import { KeyringAddress } from '@polkadot/ui-keyring/types';
|
||||
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
export { AppProps as ComponentProps } from '@polkadot/react-components/types';
|
||||
|
||||
export interface BareProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface I18nProps extends BareProps, WithTranslation {}
|
||||
|
||||
export interface ModalProps {
|
||||
onClose: () => void;
|
||||
onStatusChange: (status: ActionStatus) => void;
|
||||
}
|
||||
|
||||
export interface SortedAccount {
|
||||
account: KeyringAddress;
|
||||
children: SortedAccount[];
|
||||
isFavorite: boolean;
|
||||
}
|
||||
|
||||
export interface AmountValidateState {
|
||||
error: string | null;
|
||||
warning: string | null;
|
||||
}
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { ActionStatus } from '@polkadot/react-components/Status/types';
|
||||
import { Balance, Conviction } from '@polkadot/types/interfaces';
|
||||
import { KeyringAddress } from '@polkadot/ui-keyring/types';
|
||||
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
@ -18,3 +20,16 @@ export interface ModalProps {
|
||||
onClose: () => void;
|
||||
onStatusChange: (status: ActionStatus) => void;
|
||||
}
|
||||
|
||||
export interface Delegation {
|
||||
accountDelegated: string
|
||||
amount: Balance
|
||||
conviction: Conviction
|
||||
}
|
||||
|
||||
export interface SortedAccount {
|
||||
account: KeyringAddress;
|
||||
children: SortedAccount[];
|
||||
delegation?: Delegation;
|
||||
isFavorite: boolean;
|
||||
}
|
||||
|
||||
@ -2,8 +2,12 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { SortedAccount } from './types';
|
||||
|
||||
import React from 'react';
|
||||
import { Menu } from '@polkadot/react-components';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
import { KeyringAddress } from '@polkadot/ui-keyring/types';
|
||||
|
||||
export function createMenuGroup (items: (React.ReactNode | false | undefined | null)[]): React.ReactNode | null {
|
||||
const filtered = items.filter((item): item is React.ReactNode => !!item);
|
||||
@ -12,3 +16,50 @@ export function createMenuGroup (items: (React.ReactNode | false | undefined | n
|
||||
? <>{filtered}<Menu.Divider /></>
|
||||
: null;
|
||||
}
|
||||
|
||||
function expandList (mapped: SortedAccount[], entry: SortedAccount): SortedAccount[] {
|
||||
mapped.push(entry);
|
||||
|
||||
entry.children.forEach((entry): void => {
|
||||
expandList(mapped, entry);
|
||||
});
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
export function sortAccounts (addresses: string[], favorites: string[]): SortedAccount[] {
|
||||
const mapped = addresses
|
||||
.map((address) => keyring.getAccount(address))
|
||||
.filter((account): account is KeyringAddress => !!account)
|
||||
.map((account): SortedAccount => ({
|
||||
account,
|
||||
children: [],
|
||||
isFavorite: favorites.includes(account.address)
|
||||
}))
|
||||
.sort((a, b) => (a.account.meta.whenCreated || 0) - (b.account.meta.whenCreated || 0));
|
||||
|
||||
return mapped
|
||||
.filter((entry): boolean => {
|
||||
const parentAddress = entry.account.meta.parentAddress;
|
||||
|
||||
if (parentAddress) {
|
||||
const parent = mapped.find(({ account: { address } }) => address === parentAddress);
|
||||
|
||||
if (parent) {
|
||||
parent.children.push(entry);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.reduce(expandList, [])
|
||||
.sort((a, b): number =>
|
||||
a.isFavorite === b.isFavorite
|
||||
? 0
|
||||
: b.isFavorite
|
||||
? 1
|
||||
: -1
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import { PropIndex, Proposal } from '@polkadot/types/interfaces';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Dropdown, Modal, ProposedAction, VoteAccount, VoteActions, VoteToggle, VoteValue } from '@polkadot/react-components';
|
||||
import { Button, ConvictionDropdown, Modal, ProposedAction, VoteAccount, VoteActions, VoteToggle, VoteValue } from '@polkadot/react-components';
|
||||
import { useAccounts, useApi, useToggle } from '@polkadot/react-hooks';
|
||||
import { isBoolean } from '@polkadot/util';
|
||||
|
||||
@ -17,8 +17,6 @@ interface Props {
|
||||
referendumId: PropIndex;
|
||||
}
|
||||
|
||||
const CONVICTIONS: [number, number][] = [1, 2, 4, 8, 16, 32].map((lock, index) => [index + 1, lock]);
|
||||
|
||||
function Voting ({ proposal, referendumId }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
@ -38,24 +36,6 @@ function Voting ({ proposal, referendumId }: Props): React.ReactElement<Props> |
|
||||
[api]
|
||||
);
|
||||
|
||||
const [enact] = useState(
|
||||
(api.consts.democracy.enactmentPeriod.toNumber() * api.consts.timestamp.minimumPeriod.toNumber() / 1000 * 2) / 60 / 60 / 24
|
||||
);
|
||||
|
||||
const convictionOpts = useMemo(() => [
|
||||
{ text: t<string>('0.1x voting balance, no lockup period'), value: 0 },
|
||||
...CONVICTIONS.map(([value, lock]): { text: string; value: number } => ({
|
||||
text: t<string>('{{value}}x voting balance, locked for {{lock}}x enactment ({{period}} days)', {
|
||||
replace: {
|
||||
lock,
|
||||
period: (enact * lock).toFixed(2),
|
||||
value
|
||||
}
|
||||
}),
|
||||
value
|
||||
}))
|
||||
], [t, enact]);
|
||||
|
||||
const _onChangeVote = useCallback(
|
||||
(vote?: boolean): void => setVoteValue(isBoolean(vote) ? vote : true),
|
||||
[]
|
||||
@ -101,11 +81,10 @@ function Voting ({ proposal, referendumId }: Props): React.ReactElement<Props> |
|
||||
onChange={setBalance}
|
||||
/>
|
||||
)}
|
||||
<Dropdown
|
||||
<ConvictionDropdown
|
||||
help={t<string>('The conviction to use for this vote, with an appropriate lock period.')}
|
||||
label={t<string>('conviction')}
|
||||
onChange={setConviction}
|
||||
options={convictionOpts}
|
||||
value={conviction}
|
||||
/>
|
||||
</Modal.Column>
|
||||
|
||||
@ -213,8 +213,9 @@ export default React.memo(styled(AddressMini)`
|
||||
|
||||
.ui--AddressMini-summary {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.2;
|
||||
margin-left: 2.25rem;
|
||||
margin-top: -0.5rem;
|
||||
margin-top: -0.2rem;
|
||||
text-align: left;
|
||||
}
|
||||
`);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user