Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 79 additions & 1 deletion packages/app-accounts/src/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@

import { DeriveAccountInfo } from '@polkadot/api-derive/types';
import { ActionStatus } from '@polkadot/react-components/Status/types';
import { RecoveryConfig } from '@polkadot/types/interfaces';

import React, { useState, useEffect } from 'react';
import { Label } from 'semantic-ui-react';
import styled from 'styled-components';
import { AddressInfo, AddressSmall, Button, ChainLock, Forget, Icon, InputTags, LinkPolkascan, Menu, Popup, Input } from '@polkadot/react-components';
import { AddressInfo, AddressSmall, Badge, Button, ChainLock, Forget, Icon, IdentityIcon, InputTags, LinkPolkascan, Menu, Popup, Input } from '@polkadot/react-components';
import { useApi, useCall, useToggle } from '@polkadot/react-hooks';
import { Option } from '@polkadot/types';
import keyring from '@polkadot/ui-keyring';
import { formatBalance, formatNumber } from '@polkadot/util';

import Backup from './modals/Backup';
import ChangePass from './modals/ChangePass';
import Derive from './modals/Derive';
import Identity from './modals/Identity';
import RecoverAccount from './modals/RecoverAccount';
import RecoverSetup from './modals/RecoverSetup';
import Transfer from './modals/Transfer';
import { useTranslation } from './translate';

Expand All @@ -31,6 +36,10 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
const { t } = useTranslation();
const api = useApi();
const info = useCall<DeriveAccountInfo>(api.api.derive.accounts.info as any, [address]);
const recoveryInfo = useCall<RecoveryConfig | null>(api.api.query.recovery?.recoverable, [address], {
transform: (opt: Option<RecoveryConfig>): RecoveryConfig | null =>
opt.unwrapOr(null)
});
const [tags, setTags] = useState<string[]>([]);
const [accName, setAccName] = useState('');
const [genesisHash, setGenesisHash] = useState<string | null>(null);
Expand All @@ -43,6 +52,8 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
const [isForgetOpen, toggleForget] = useToggle();
const [isIdentityOpen, toggleIdentity] = useToggle();
const [isPasswordOpen, togglePassword] = useToggle();
const [isRecoverAccountOpen, toggleRecoverAccount] = useToggle();
const [isRecoverSetupOpen, toggleRecoverSetup] = useToggle();
const [isSettingsOpen, toggleSettings] = useToggle();
const [isTransferOpen, toggleTransfer] = useToggle();

Expand Down Expand Up @@ -158,6 +169,46 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
onClick={_onFavorite}
/>
</td>
<td className='together'>
{recoveryInfo && (
<Badge
hover={
<div>
<p>{t('This account is recoverable, with the following friends:')}</p>
<div>
{recoveryInfo.friends.map((friend, index): React.ReactNode => (
<IdentityIcon
key={index}
size={24}
value={friend}
/>
))}
</div>
<table>
<tbody>
<tr>
<td>{t('threshold')}</td>
<td>{formatNumber(recoveryInfo.threshold)}</td>
</tr>
<tr>
<td>{t('delay')}</td>
<td>{formatNumber(recoveryInfo.delayPeriod)}</td>
</tr>
<tr>
<td>{t('deposit')}</td>
<td>{formatBalance(recoveryInfo.deposit)}</td>
</tr>
</tbody>
</table>
</div>
}
info={<Icon name='shield' />}
isInline
isTooltip
type='online'
/>
)}
</td>
<td className='top'>
<AddressSmall
overrideName={
Expand Down Expand Up @@ -222,6 +273,20 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
senderId={address}
/>
)}
{isRecoverAccountOpen && (
<RecoverAccount
address={address}
key='recover-account'
onClose={toggleRecoverAccount}
/>
)}
{isRecoverSetupOpen && (
<RecoverSetup
address={address}
key='recover-setup'
onClose={toggleRecoverSetup}
/>
)}
</td>
<td className='top'>
{isEditingTags
Expand Down Expand Up @@ -321,6 +386,19 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
>
{t('Forget this account')}
</Menu.Item>
{api.api.tx.recovery?.createRecovery && (
<>
<Menu.Divider />
{!recoveryInfo && (
<Menu.Item onClick={toggleRecoverSetup}>
{t('Make recoverable')}
</Menu.Item>
)}
<Menu.Item onClick={toggleRecoverAccount}>
{t('Initiate recovery for another')}
</Menu.Item>
</>
)}
{!api.isDevelopment && (
<>
<Menu.Divider />
Expand Down
52 changes: 52 additions & 0 deletions packages/app-accounts/src/modals/RecoverAccount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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, { useState } from 'react';
import { InputAddress, Modal, TxButton } from '@polkadot/react-components';

import { useTranslation } from '../translate';

interface Props {
address: string;
className?: string;
onClose: () => void;
}

export default function RecoverAccount ({ address, className, onClose }: Props): React.ReactElement {
const { t } = useTranslation();
const [recover, setRecover] = useState<string | null>(null);

return (
<Modal
className={className}
header={t('Initiate account recovery')}
>
<Modal.Content>
<InputAddress
isDisabled
label={t('the account to recover to')}
value={address}
/>
<InputAddress
help={t('Select the account you wish to recover into this account.')}
label={t('recover this account')}
onChange={setRecover}
type='allPlus'
/>
</Modal.Content>
<Modal.Actions onCancel={onClose}>
<TxButton
accountId={address}
icon='recycle'
isDisabled={!recover || recover === address}
label={t('Start recovery')}
onClick={onClose}
params={[recover]}
tx='recovery.initiateRecovery'
withSpinner={false}
/>
</Modal.Actions>
</Modal>
);
}
88 changes: 88 additions & 0 deletions packages/app-accounts/src/modals/RecoverSetup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// 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 BN from 'bn.js';
import React, { useEffect, useState } from 'react';
import { AddressMulti, InputAddress, InputNumber, Modal, TxButton } from '@polkadot/react-components';
import { useAccounts, useAddresses } from '@polkadot/react-hooks';

import { useTranslation } from '../translate';

interface Props {
address: string;
className?: string;
onClose: () => void;
}

const MAX_HELPERS = 16;

export default function RecoverSetup ({ address, className, onClose }: Props): React.ReactElement {
const { t } = useTranslation();
const { allAccounts } = useAccounts();
const { allAddresses } = useAddresses();
const [availableHelpers, setAvailableHelpers] = useState<string[]>([]);
const [delay, setDelay] = useState<BN | undefined>();
const [helpers, setHelpers] = useState<string[]>([]);
const [threshold, setThreshold] = useState<BN | undefined>();

useEffect((): void => {
if (allAccounts && allAddresses) {
setAvailableHelpers(
[...allAccounts, ...allAddresses].filter((a): boolean => a !== address)
);
}
}, [address, allAccounts, allAddresses]);

const isErrorDelay = !delay;
const isErrorHelpers = !helpers.length;
const isErrorThreshold = !threshold || !threshold.gtn(0) || threshold.gtn(helpers.length);

return (
<Modal
className={className}
header={t('Setup account as recoverable')}
>
<Modal.Content>
<InputAddress
isDisabled
label={t('the account to make recoverable')}
value={address}
/>
<AddressMulti
available={availableHelpers}
help={t('The addresses that are able to help in recovery. You can select up to {{maxHelpers}} trusted helpers.', { replace: { maxHelpers: MAX_HELPERS } })}
label={t('trusted social recovery helpers')}
onChange={setHelpers}
maxCount={MAX_HELPERS}
value={helpers}
/>
<InputNumber
help={t('The threshold of vouches that is to be reached for the account to be recovered.')}
isError={isErrorThreshold}
label={t('recoverey threshold')}
onChange={setThreshold}
/>
<InputNumber
help={t('The delay between vouching and the availability of the recovered account.')}
isError={isErrorDelay}
isZeroable
label={t('recoverey block delay')}
onChange={setDelay}
/>
</Modal.Content>
<Modal.Actions onCancel={onClose}>
<TxButton
accountId={address}
icon='share alternate'
isDisabled={isErrorHelpers || isErrorThreshold || isErrorDelay}
label={t('Make recoverable')}
onClick={onClose}
params={[helpers, threshold, delay]}
tx='recovery.createRecovery'
withSpinner={false}
/>
</Modal.Actions>
</Modal>
);
}
4 changes: 2 additions & 2 deletions packages/react-components/src/Status/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export default styled(Status)`
position: fixed;
right: 0.25rem;
top: 0.25rem;
width: 20rem;
width: 23rem;
z-index: 1001;

.dismiss {
Expand Down Expand Up @@ -233,7 +233,7 @@ export default styled(Status)`
.short {
font-size: 2.5rem;
opacity: 0.75;
padding: 0.5rem;
padding: 0.5rem 0 0.5rem 0.5rem;

i.icon {
line-height: 1;
Expand Down