diff --git a/packages/page-accounts/src/modals/MultisigCreate.tsx b/packages/page-accounts/src/modals/MultisigCreate.tsx index e4007a917d..a35fcdfb9d 100644 --- a/packages/page-accounts/src/modals/MultisigCreate.tsx +++ b/packages/page-accounts/src/modals/MultisigCreate.tsx @@ -5,11 +5,13 @@ import type { ActionStatus } from '@polkadot/react-components/Status/types'; import type { ModalProps } from '../types'; import React, { useCallback, useState } from 'react'; +import styled from 'styled-components'; -import { Button, Input, InputAddressMulti, InputNumber, Modal } from '@polkadot/react-components'; +import { AddressMini, Button, IconLink, Input, InputAddressMulti, InputFile, InputNumber, Labelled, MarkError, Modal, Toggle } from '@polkadot/react-components'; import { useApi } from '@polkadot/react-hooks'; import { keyring } from '@polkadot/ui-keyring'; -import { BN } from '@polkadot/util'; +import { assert, BN, u8aToString } from '@polkadot/util'; +import { validateAddress } from '@polkadot/util-crypto'; import useKnownAddresses from '../Accounts/useKnownAddresses'; import { useTranslation } from '../translate'; @@ -26,9 +28,41 @@ interface CreateOptions { tags?: string[]; } +interface UploadedFileData { + isUploadedFileValid: boolean; + uploadedFileError: string; + uploadedSignatories: string[]; +} + const MAX_SIGNATORIES = 16; const BN_TWO = new BN(2); +const acceptedFormats = ['application/json'].join(', '); + +function parseFile (file: Uint8Array): UploadedFileData { + let uploadError = ''; + let items: string[]; + + try { + items = JSON.parse(u8aToString(file)) as string[]; + assert(Array.isArray(items) && !!items.length, 'JSON file should contain an array of signatories'); + + items = items.filter((item) => validateAddress(item)); + items = [...new Set(items)]; // remove duplicates + + assert(items.length <= MAX_SIGNATORIES, `Maximum you can have ${MAX_SIGNATORIES} signatories`); + } catch (error) { + items = []; + uploadError = (error as Error).message ? (error as Error).message : (error as Error).toString(); + } + + return { + isUploadedFileValid: !uploadError, + uploadedFileError: uploadError, + uploadedSignatories: items + }; +} + function createMultisig (signatories: string[], threshold: BN | number, { genesisHash, name, tags = [] }: CreateOptions, success: string): ActionStatus { // we will fill in all the details below const status = { action: 'create' } as ActionStatus; @@ -53,7 +87,13 @@ function Multisig ({ className = '', onClose, onStatusChange }: Props): React.Re const { t } = useTranslation(); const availableSignatories = useKnownAddresses(); const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' }); + const [{ isUploadedFileValid, uploadedFileError, uploadedSignatories }, setUploadedFile] = useState({ + isUploadedFileValid: true, + uploadedFileError: '', + uploadedSignatories: [] + }); const [signatories, setSignatories] = useState(['']); + const [showSignaturesUpload, setShowSignaturesUpload] = useState(false); const [{ isThresholdValid, threshold }, setThreshold] = useState({ isThresholdValid: true, threshold: BN_TWO }); const _createMultisig = useCallback( @@ -78,6 +118,38 @@ function Multisig ({ className = '', onClose, onStatusChange }: Props): React.Re [signatories] ); + const _onChangeFile = useCallback( + (file: Uint8Array) => { + const fileData = parseFile(file); + + setUploadedFile(fileData); + + if (fileData.isUploadedFileValid || uploadedSignatories.length) { + setSignatories(fileData.uploadedSignatories.length ? fileData.uploadedSignatories : ['']); + } + }, + [uploadedSignatories] + ); + + const resetFileUpload = useCallback( + () => { + setUploadedFile({ + isUploadedFileValid, + uploadedFileError, + uploadedSignatories: [] + }); + }, + [uploadedFileError, isUploadedFileValid] + ); + + const _onChangeAddressMulti = useCallback( + (items: string[]) => { + resetFileUpload(); + setSignatories(items); + }, + [resetFileUpload] + ); + const isValid = isNameValid && isThresholdValid; return ( @@ -88,24 +160,74 @@ function Multisig ({ className = '', onClose, onStatusChange }: Props): React.Re size='large' > - -

{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.')}

-

{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.')}

- - } - > - ('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')} + + ('Upload JSON file with signatories')} + onChange={setShowSignaturesUpload} + value={showSignaturesUpload} /> + {!showSignaturesUpload && ( + +

{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.')}

+

{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.')}

+ + } + > + ('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={_onChangeAddressMulti} + value={signatories} + valueLabel={t('selected signatories')} + /> +
+ )} + {showSignaturesUpload && ( + ('Supply a JSON file with the list of signatories.')}> + ('Select a JSON key file with the list of signatories.')} + isError={!isUploadedFileValid} + label={t('upload signatories list')} + onChange={_onChangeFile} + withLabel + /> + {!!uploadedSignatories.length && ( + ('found signatories')} + labelExtra={( + ('Reset')} + onClick={resetFileUpload} + /> + )} + > +
+ {uploadedSignatories.map((address): React.ReactNode => ( +
+ +
+ ))} +
+
+ )} + {uploadedFileError && ( + + )} +
+ )} ('The threshold for approval should be less or equal to the number of signatories for this multisig.')}> ('The threshold for this multisig')} @@ -139,4 +261,9 @@ function Multisig ({ className = '', onClose, onStatusChange }: Props): React.Re ); } -export default React.memo(Multisig); +export default React.memo(styled(Multisig)` + .signaturesFileToggle { + width: 100%; + text-align: right; + } +`);