Multisig creation layout (#2674)

* Multisig creation layout

* Header cleanups

* Cleanup flag display
This commit is contained in:
Jaco Greeff 2020-05-02 08:56:16 +02:00 committed by GitHub
parent eb246c3ab5
commit a66db1f152
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 152 additions and 109 deletions

View File

@ -1,4 +1,4 @@
// Copyright 2017-2020 @polkadot/app-staking authors & contributors
// Copyright 2017-2020 @polkadot/app-accounts authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

View File

@ -1,4 +1,4 @@
// Copyright 2017-2020 @polkadot/app-staking authors & contributors
// Copyright 2017-2020 @polkadot/app-accounts authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

View File

@ -6,12 +6,13 @@ import { ActionStatus } from '@polkadot/react-components/Status/types';
import { ModalProps } from '../../types';
import BN from 'bn.js';
import React, { useCallback, useMemo, useState } from 'react';
import { Button, Input, InputAddress, InputNumber, Modal } from '@polkadot/react-components';
import React, { useCallback, useState } from 'react';
import { Button, Input, InputAddressMulti, InputNumber, Modal } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import keyring from '@polkadot/ui-keyring';
import { useTranslation } from '../../translate';
import useKnownAddresses from '../useKnownAddresses';
interface Props extends ModalProps {
className?: string;
@ -24,8 +25,6 @@ interface CreateOptions {
tags?: string[];
}
type Setter = (address: string| null) => void;
const MAX_SIGNATORIES = 16;
function createAccount (signatories: string[], threshold: BN | number, { genesisHash, name, tags = [] }: CreateOptions, success: string): ActionStatus {
@ -50,23 +49,10 @@ function createAccount (signatories: string[], threshold: BN | number, { genesis
function Multisig ({ className, onClose, onStatusChange }: Props): React.ReactElement<Props> {
const { api, isDevelopment } = useApi();
const { t } = useTranslation();
const availableSignatories = useKnownAddresses();
const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
const [signatories, setSignatories] = useState<string[]>(['']);
const [{ isThresholdValid, threshold }, setThreshold] = useState({ isThresholdValid: true, threshold: new BN(1) });
const setSignatory = useMemo((): Setter[] => {
return Array(MAX_SIGNATORIES).fill(0).map((_, index): Setter =>
(address: string | null): void => {
setSignatories((signatories: string[]): string[] => {
if (address) {
signatories[index] = address;
}
return signatories;
});
}
);
}, []);
const isValid = isNameValid && isThresholdValid;
const _createMultisig = useCallback(
(): void => {
@ -90,59 +76,61 @@ function Multisig ({ className, onClose, onStatusChange }: Props): React.ReactEl
[signatories]
);
const _onLess = useCallback(
() => setSignatories((signatories) => signatories.slice(0, signatories.length - 1)),
[]
);
const _onMore = useCallback(
() => setSignatories((signatories) => signatories.concat('')),
[]
);
const isValid = isNameValid && isThresholdValid;
return (
<Modal
className={className}
header={t('Add multisig')}
size='large'
>
<Modal.Content>
<Input
autoFocus
className='full'
help={t('Name given to this multisig. You can edit it it any later point in time.')}
isError={!isNameValid}
label={t('name')}
onChange={_onChangeName}
placeholder={t('multisig name')}
/>
<InputNumber
help={t('The threshold for this multisig')}
isError={!isThresholdValid}
label={t('threshold')}
onChange={_onChangeThreshold}
/>
{signatories.map((_, index) => (
<InputAddress
key={index}
label={t('signatory {{index}}/{{length}}', { replace: { index: index + 1, length: signatories.length } })}
onChange={setSignatory[index]}
value={signatories[index]}
/>
))}
<Button.Group>
<Button
icon='minus'
isDisabled={signatories.length === 1}
label={t('Less')}
onClick={_onLess}
/>
<Button
icon='plus'
isDisabled={signatories.length === MAX_SIGNATORIES}
label={t('More')}
onClick={_onMore}
/>
</Button.Group>
<Modal.Columns>
<Modal.Column>
<Input
autoFocus
className='full'
help={t('Name given to this multisig. You can edit it at any later point in time.')}
isError={!isNameValid}
label={t('name')}
onChange={_onChangeName}
placeholder={t('multisig name')}
/>
</Modal.Column>
<Modal.Column>
<p>{t('The name is for unique identification of the account in your owner lists.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<InputAddressMulti
available={availableSignatories}
availableLabel={t('available signatories')}
help={t('The addresses that are able to approve multisig transactions. You can select up to {{maxHelpers}} trusted addresses.', { replace: { maxHelpers: MAX_SIGNATORIES } })}
maxCount={MAX_SIGNATORIES}
onChange={setSignatories}
value={signatories}
valueLabel={t('selected signatories')}
/>
</Modal.Column>
<Modal.Column>
<p>{t('The signatories has the ability to create transactions using the multisig and approve transactions sent by others.Once the threshold is reached with approvals, the multisig transaction is enacted on-chain.')}</p>
<p>{t('Since the multisig function like any other account, once created it is available for selection anywhere accounts are used and needs to be funded before use.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<InputNumber
help={t('The threshold for this multisig')}
isError={!isThresholdValid}
label={t('threshold')}
onChange={_onChangeThreshold}
/>
</Modal.Column>
<Modal.Column>
<p>{t('The threshold for approval should be less or equal to the number of signatories for this multisig.')}</p>
</Modal.Column>
</Modal.Columns>
</Modal.Content>
<Modal.Actions onCancel={onClose}>
<Button

View File

@ -1,4 +1,4 @@
// Copyright 2017-2020 @polkadot/app-staking authors & contributors
// Copyright 2017-2020 @polkadot/app-accounts authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

View File

@ -1,13 +1,13 @@
// Copyright 2017-2020 @polkadot/app-staking authors & contributors
// Copyright 2017-2020 @polkadot/app-accounts 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 React, { useState } from 'react';
import { InputAddressMulti, InputAddress, InputNumber, Modal, TxButton } from '@polkadot/react-components';
import { useAccounts, useAddresses } from '@polkadot/react-hooks';
import { useTranslation } from '../../translate';
import useKnownAddresses from '../useKnownAddresses';
interface Props {
address: string;
@ -19,19 +19,11 @@ const MAX_HELPERS = 16;
function RecoverSetup ({ address, className, onClose }: Props): React.ReactElement {
const { t } = useTranslation();
const { allAccounts } = useAccounts();
const { allAddresses } = useAddresses();
const [availableHelpers, setAvailableHelpers] = useState<string[]>([]);
const availableHelpers = useKnownAddresses(address);
const [delay, setDelay] = useState<BN | undefined>();
const [helpers, setHelpers] = useState<string[]>([]);
const [threshold, setThreshold] = useState<BN | undefined>();
useEffect((): void => {
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);
@ -40,35 +32,58 @@ function RecoverSetup ({ address, className, onClose }: Props): React.ReactEleme
<Modal
className={className}
header={t('Setup account as recoverable')}
size='large'
>
<Modal.Content>
<InputAddress
isDisabled
label={t('the account to make recoverable')}
value={address}
/>
<InputAddressMulti
available={availableHelpers}
availableLabel={t('available social recovery helpers')}
help={t('The addresses that are able to help in recovery. You can select up to {{maxHelpers}} trusted helpers.', { replace: { maxHelpers: MAX_HELPERS } })}
maxCount={MAX_HELPERS}
onChange={setHelpers}
value={helpers}
valueLabel={t('trusted social recovery helpers')}
/>
<InputNumber
help={t('The threshold of vouches that is to be reached for the account to be recovered.')}
isError={isErrorThreshold}
label={t('recovery threshold')}
onChange={setThreshold}
/>
<InputNumber
help={t('The delay between vouching and the availability of the recovered account.')}
isError={isErrorDelay}
isZeroable
label={t('recovery block delay')}
onChange={setDelay}
/>
<Modal.Columns>
<Modal.Column>
<InputAddress
isDisabled
label={t('the account to make recoverable')}
value={address}
/>
</Modal.Column>
<Modal.Column>
<p>{t('The recoverable account is protected against the loss of seed/access by a social process.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<InputAddressMulti
available={availableHelpers}
availableLabel={t('available social recovery helpers')}
help={t('The addresses that are able to help in recovery. You can select up to {{maxHelpers}} trusted helpers.', { replace: { maxHelpers: MAX_HELPERS } })}
maxCount={MAX_HELPERS}
onChange={setHelpers}
value={helpers}
valueLabel={t('trusted social recovery helpers')}
/>
</Modal.Column>
<Modal.Column>
<p>{t('These are trusted individuals that can verify and approve any recovery actions. With recovery, once the threshold is reached, the funds associated with the account can be moved to a new destination.')}</p>
<p>{t('The helpers should be able to verify, via an off-chain mechanism, that the account owner indeed wishes to recover access and as such provide any approvals. In the cases of malicious recovery procedures, they will have the power to stop it.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<InputNumber
help={t('The threshold of vouches that is to be reached for the account to be recovered.')}
isError={isErrorThreshold}
label={t('recovery threshold')}
onChange={setThreshold}
/>
<InputNumber
help={t('The delay between vouching and the availability of the recovered account.')}
isError={isErrorDelay}
isZeroable
label={t('recovery block delay')}
onChange={setDelay}
/>
</Modal.Column>
<Modal.Column>
<p>{t('The threshold for approvals and the delay is the protection associated with the account. The delay should be such that any colluding recovery attempts does have a window to stop.')}</p>
</Modal.Column>
</Modal.Columns>
</Modal.Content>
<Modal.Actions onCancel={onClose}>
<TxButton

View File

@ -1,4 +1,4 @@
// Copyright 2017-2020 @polkadot/app-staking authors & contributors
// Copyright 2017-2020 @polkadot/app-accounts authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

View File

@ -0,0 +1,20 @@
// Copyright 2017-2020 @polkadot/app-accounts 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 { useEffect, useState } from 'react';
import { useAccounts, useAddresses } from '@polkadot/react-hooks';
export default function useKnownAddresses (exclude?: string): string[] {
const { allAccounts } = useAccounts();
const { allAddresses } = useAddresses();
const [known, setKnown] = useState<string[]>([]);
useEffect((): void => {
allAccounts && allAddresses && setKnown(
[...allAccounts, ...allAddresses].filter((a): boolean => a !== exclude)
);
}, [allAccounts, allAddresses, exclude]);
return known;
}

View File

@ -13,9 +13,9 @@ interface Props {
flags: AddressFlags;
}
function Flags ({ flags: { isCouncil, isDevelopment, isExternal, isSociety, isSudo, isTechCommittee } }: Props): React.ReactElement<Props> | null {
function Flags ({ flags: { isCouncil, isDevelopment, isExternal, isInjected, isMultisig, isSociety, isSudo, isTechCommittee } }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const hasFlags = isDevelopment || isExternal || isSociety || isCouncil || isTechCommittee || isSudo;
const hasFlags = isCouncil || isDevelopment || isExternal || isInjected || isMultisig || isSociety || isSudo || isTechCommittee;
if (!hasFlags) {
return null;
@ -24,6 +24,23 @@ function Flags ({ flags: { isCouncil, isDevelopment, isExternal, isSociety, isSu
return (
<div className='ui--AddressMenu-flags'>
{isExternal && (
isMultisig
? (
<Tag
color='green'
label={t('Multisig')}
size='tiny'
/>
)
: (
<Tag
color='grey'
label={t('External')}
size='tiny'
/>
)
)}
{isInjected && (
<Tag
color='grey'
label={t('Injected')}

View File

@ -80,6 +80,7 @@ export interface AddressFlags extends DeriveAccountFlags {
isExternal: boolean;
isFavorite: boolean;
isInContacts: boolean;
isInjected: boolean;
isMultisig: boolean;
isOwned: boolean;
}

View File

@ -22,6 +22,7 @@ const IS_NONE = {
isExternal: false,
isFavorite: false,
isInContacts: false,
isInjected: false,
isMultisig: false,
isOwned: false,
isSociety: false,
@ -99,6 +100,7 @@ export default function useAccountInfo (value: string): UseAccountInfo {
isEditable: (!identity?.display && (isInContacts || accountOrAddress?.meta.isMultisig || (accountOrAddress && !(accountOrAddress.meta.isInjected || accountOrAddress.meta.isHardware)))) || false,
isExternal: accountOrAddress?.meta.isExternal || false,
isInContacts,
isInjected: accountOrAddress?.meta.isInjected || false,
isMultisig: accountOrAddress?.meta.isMultisig || false,
isOwned
}));