Social account recovery (#2187)

* Social account recovery ... start

* Dialogs with send buttons

* TxButton icons (before I forget)

* Recovery start

* withSpinner
This commit is contained in:
Jaco Greeff 2020-01-24 10:52:06 +01:00 committed by GitHub
parent a5ea8d1bf9
commit 63d22395ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 221 additions and 3 deletions

View File

@ -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';
@ -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);
@ -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();
@ -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={
@ -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
@ -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 />

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -186,7 +186,7 @@ export default styled(Status)`
position: fixed;
right: 0.25rem;
top: 0.25rem;
width: 20rem;
width: 23rem;
z-index: 1001;
.dismiss {
@ -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;