Multisig approvals (#2627)
This commit is contained in:
parent
249db4b356
commit
b536e8e11a
@ -4,15 +4,15 @@
|
|||||||
|
|
||||||
import { DeriveBalancesAll } from '@polkadot/api-derive/types';
|
import { DeriveBalancesAll } from '@polkadot/api-derive/types';
|
||||||
import { ActionStatus } from '@polkadot/react-components/Status/types';
|
import { ActionStatus } from '@polkadot/react-components/Status/types';
|
||||||
import { RecoveryConfig } from '@polkadot/types/interfaces';
|
import { H256, Multisig, RecoveryConfig } from '@polkadot/types/interfaces';
|
||||||
import { SortedAccount } from './types';
|
import { SortedAccount } from './types';
|
||||||
|
|
||||||
import BN from 'bn.js';
|
import BN from 'bn.js';
|
||||||
import React, { useCallback, useState, useEffect } from 'react';
|
import React, { useCallback, useState, useEffect } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { AddressInfo, AddressMini, AddressSmall, Badge, Button, ChainLock, CryptoType, Forget, Icon, IdentityIcon, LinkExternal, Menu, Popup, Tag } from '@polkadot/react-components';
|
import { AddressInfo, AddressMini, AddressSmall, Badge, Button, ChainLock, CryptoType, Forget, Icon, IdentityIcon, LinkExternal, Menu, Popup, Tag } from '@polkadot/react-components';
|
||||||
import { useAccountInfo, useApi, useCall, useToggle } from '@polkadot/react-hooks';
|
import { useAccountInfo, useApi, useCall, useIncrement, useToggle } from '@polkadot/react-hooks';
|
||||||
import { Option } from '@polkadot/types';
|
import { Option, StorageKey } from '@polkadot/types';
|
||||||
import keyring from '@polkadot/ui-keyring';
|
import keyring from '@polkadot/ui-keyring';
|
||||||
import { formatBalance, formatNumber } from '@polkadot/util';
|
import { formatBalance, formatNumber } from '@polkadot/util';
|
||||||
|
|
||||||
@ -21,6 +21,7 @@ import Backup from './modals/Backup';
|
|||||||
import ChangePass from './modals/ChangePass';
|
import ChangePass from './modals/ChangePass';
|
||||||
import Derive from './modals/Derive';
|
import Derive from './modals/Derive';
|
||||||
import Identity from './modals/Identity';
|
import Identity from './modals/Identity';
|
||||||
|
import MultisigApprove from './modals/MultisigApprove';
|
||||||
import RecoverAccount from './modals/RecoverAccount';
|
import RecoverAccount from './modals/RecoverAccount';
|
||||||
import RecoverSetup from './modals/RecoverSetup';
|
import RecoverSetup from './modals/RecoverSetup';
|
||||||
import Transfer from './modals/Transfer';
|
import Transfer from './modals/Transfer';
|
||||||
@ -47,16 +48,24 @@ function calcVisible (filter: string, name: string, tags: string[]): boolean {
|
|||||||
function Account ({ account: { address, meta }, className, filter, isFavorite, setBalance, toggleFavorite }: Props): React.ReactElement<Props> | null {
|
function Account ({ account: { address, meta }, className, filter, isFavorite, setBalance, toggleFavorite }: Props): React.ReactElement<Props> | null {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
const [multiInc, refreshMulti] = useIncrement();
|
||||||
const balancesAll = useCall<DeriveBalancesAll>(api.api.derive.balances.all, [address]);
|
const balancesAll = useCall<DeriveBalancesAll>(api.api.derive.balances.all, [address]);
|
||||||
const recoveryInfo = useCall<RecoveryConfig | null>(api.api.query.recovery?.recoverable, [address], {
|
const recoveryInfo = useCall<RecoveryConfig | null>(api.api.query.recovery?.recoverable, [address], {
|
||||||
transform: (opt: Option<RecoveryConfig>) => opt.unwrapOr(null)
|
transform: (opt: Option<RecoveryConfig>) => opt.unwrapOr(null)
|
||||||
});
|
});
|
||||||
const { flags: { isDevelopment, isEditable, isExternal }, genesisHash, name: accName, onSaveGenesisHash, tags } = useAccountInfo(address);
|
const multiInfos = useCall<[H256, Multisig][]>(multiInc && api.api.query.utility?.multisigs.entries as any, [address], {
|
||||||
|
transform: (infos: [StorageKey, Option<Multisig>][]): [H256, Multisig][] =>
|
||||||
|
infos
|
||||||
|
.filter(([, opt]) => opt.isSome)
|
||||||
|
.map(([key, opt]) => [key.args[1] as H256, opt.unwrap()])
|
||||||
|
});
|
||||||
|
const { flags: { isDevelopment, isEditable, isExternal, isMultisig }, genesisHash, name: accName, onSaveGenesisHash, tags } = useAccountInfo(address);
|
||||||
const [isVisible, setIsVisible] = useState(true);
|
const [isVisible, setIsVisible] = useState(true);
|
||||||
const [isBackupOpen, toggleBackup] = useToggle();
|
const [isBackupOpen, toggleBackup] = useToggle();
|
||||||
const [isDeriveOpen, toggleDerive] = useToggle();
|
const [isDeriveOpen, toggleDerive] = useToggle();
|
||||||
const [isForgetOpen, toggleForget] = useToggle();
|
const [isForgetOpen, toggleForget] = useToggle();
|
||||||
const [isIdentityOpen, toggleIdentity] = useToggle();
|
const [isIdentityOpen, toggleIdentity] = useToggle();
|
||||||
|
const [isMultisigOpen, toggleMultisig] = useToggle();
|
||||||
const [isPasswordOpen, togglePassword] = useToggle();
|
const [isPasswordOpen, togglePassword] = useToggle();
|
||||||
const [isRecoverAccountOpen, toggleRecoverAccount] = useToggle();
|
const [isRecoverAccountOpen, toggleRecoverAccount] = useToggle();
|
||||||
const [isRecoverSetupOpen, toggleRecoverSetup] = useToggle();
|
const [isRecoverSetupOpen, toggleRecoverSetup] = useToggle();
|
||||||
@ -101,6 +110,14 @@ function Account ({ account: { address, meta }, className, filter, isFavorite, s
|
|||||||
[address, t]
|
[address, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const _closeMultisig = useCallback(
|
||||||
|
(): void => {
|
||||||
|
toggleMultisig();
|
||||||
|
refreshMulti();
|
||||||
|
},
|
||||||
|
[refreshMulti, toggleMultisig]
|
||||||
|
);
|
||||||
|
|
||||||
if (!isVisible) {
|
if (!isVisible) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -199,6 +216,16 @@ function Account ({ account: { address, meta }, className, filter, isFavorite, s
|
|||||||
senderId={address}
|
senderId={address}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isMultisigOpen && multiInfos && (
|
||||||
|
<MultisigApprove
|
||||||
|
address={address}
|
||||||
|
key='multisig-approve'
|
||||||
|
onClose={_closeMultisig}
|
||||||
|
ongoing={multiInfos}
|
||||||
|
threshold={meta.threshold}
|
||||||
|
who={meta.who}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isRecoverAccountOpen && (
|
{isRecoverAccountOpen && (
|
||||||
<RecoverAccount
|
<RecoverAccount
|
||||||
address={address}
|
address={address}
|
||||||
@ -311,6 +338,17 @@ function Account ({ account: { address, meta }, className, filter, isFavorite, s
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{isMultisig && (
|
||||||
|
<>
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Item
|
||||||
|
disabled={!multiInfos || !multiInfos.length}
|
||||||
|
onClick={toggleMultisig}
|
||||||
|
>
|
||||||
|
{t('Multisig approvals')}
|
||||||
|
</Menu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{!api.isDevelopment && (
|
{!api.isDevelopment && (
|
||||||
<>
|
<>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import { Button, Input, Table } from '@polkadot/react-components';
|
|||||||
import { useTranslation } from '../translate';
|
import { useTranslation } from '../translate';
|
||||||
import CreateModal from './modals/Create';
|
import CreateModal from './modals/Create';
|
||||||
import ImportModal from './modals/Import';
|
import ImportModal from './modals/Import';
|
||||||
import Multisig from './modals/Multisig';
|
import Multisig from './modals/MultisigCreate';
|
||||||
import QrModal from './modals/Qr';
|
import QrModal from './modals/Qr';
|
||||||
import Account from './Account';
|
import Account from './Account';
|
||||||
import Banner from './Banner';
|
import Banner from './Banner';
|
||||||
|
|||||||
@ -0,0 +1,96 @@
|
|||||||
|
// 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 { H256, Multisig } from '@polkadot/types/interfaces';
|
||||||
|
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Dropdown, InputAddress, Modal, TxButton } from '@polkadot/react-components';
|
||||||
|
|
||||||
|
import { useTranslation } from '../../translate';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
address: string;
|
||||||
|
className?: string;
|
||||||
|
onClose: () => void;
|
||||||
|
ongoing: [H256, Multisig][];
|
||||||
|
threshold: number;
|
||||||
|
who: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
text: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MultisigApprove ({ className, onClose, ongoing, threshold, who }: Props): React.ReactElement<Props> {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [hash, setHash] = useState<string | null>(ongoing[0][0].toHex());
|
||||||
|
const [multisig, setMultisig] = useState<[H256, Multisig] | null>(null);
|
||||||
|
const [signatory, setSignatory] = useState<string | null>(null);
|
||||||
|
const [type, setType] = useState<string | null>('aye');
|
||||||
|
const calltypes = useMemo<Option[]>(
|
||||||
|
() => [
|
||||||
|
{ text: t('Approve this call hash'), value: 'aye' },
|
||||||
|
{ text: t('Cancel this call hash'), value: 'nay' }
|
||||||
|
],
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
const hashes = useMemo<Option[]>(
|
||||||
|
() => ongoing.map(([h]) => ({ text: h.toHex(), value: h.toHex() })),
|
||||||
|
[ongoing]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect((): void => {
|
||||||
|
hash && setMultisig(
|
||||||
|
ongoing.find(([h]) => h.eq(hash)) || null
|
||||||
|
);
|
||||||
|
}, [hash, ongoing]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className={className}
|
||||||
|
header={t('Pending call hashes')}
|
||||||
|
>
|
||||||
|
<Modal.Content>
|
||||||
|
<InputAddress
|
||||||
|
filter={who}
|
||||||
|
help={t('The signatory to send the approval/cancel from')}
|
||||||
|
label={t('signatory')}
|
||||||
|
onChange={setSignatory}
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
help={t('The call hashes that have not been executed as of yet.')}
|
||||||
|
label={t('pending hashes')}
|
||||||
|
onChange={setHash}
|
||||||
|
options={hashes}
|
||||||
|
value={hash}
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
help={t('Either approve or reject this call.')}
|
||||||
|
label={t('approval type')}
|
||||||
|
onChange={setType}
|
||||||
|
options={calltypes}
|
||||||
|
value={type}
|
||||||
|
/>
|
||||||
|
</Modal.Content>
|
||||||
|
<Modal.Actions onCancel={onClose}>
|
||||||
|
<TxButton
|
||||||
|
accountId={signatory}
|
||||||
|
icon={type === 'aye' ? 'check' : 'cancel'}
|
||||||
|
isDisabled={!multisig}
|
||||||
|
label={type === 'aye' ? 'Approve' : 'Reject'}
|
||||||
|
onStart={onClose}
|
||||||
|
params={[threshold, who.filter((who) => who !== signatory), multisig ? multisig[1].when : null, hash]}
|
||||||
|
tx={
|
||||||
|
type === 'aye'
|
||||||
|
? 'utility.approveAsMulti'
|
||||||
|
: 'utility.cancelAsMulti'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Modal.Actions>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(MultisigApprove);
|
||||||
Loading…
Reference in New Issue
Block a user