Skip to content

Commit

Permalink
Allow to upload a JSON file with the list of multisig signatories (#6911
Browse files Browse the repository at this point in the history
)

* Allow to upload a JSON file with the list of multisig signatories

* Fix linter errors

* Chore

* Toggle between signatories input modes
  • Loading branch information
jsidorenko authored Feb 4, 2022
1 parent dc39190 commit 989e698
Showing 1 changed file with 146 additions and 19 deletions.
165 changes: 146 additions & 19 deletions packages/page-accounts/src/modals/MultisigCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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<UploadedFileData>({
isUploadedFileValid: true,
uploadedFileError: '',
uploadedSignatories: []
});
const [signatories, setSignatories] = useState<string[]>(['']);
const [showSignaturesUpload, setShowSignaturesUpload] = useState(false);
const [{ isThresholdValid, threshold }, setThreshold] = useState({ isThresholdValid: true, threshold: BN_TWO });

const _createMultisig = useCallback(
Expand All @@ -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 (
Expand All @@ -88,24 +160,74 @@ function Multisig ({ className = '', onClose, onStatusChange }: Props): React.Re
size='large'
>
<Modal.Content>
<Modal.Columns
hint={
<>
<p>{t<string>('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<string>('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>
</>
}
>
<InputAddressMulti
available={availableSignatories}
availableLabel={t<string>('available signatories')}
help={t<string>('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<string>('selected signatories')}
<Modal.Columns>
<Toggle
className='signaturesFileToggle'
label={t<string>('Upload JSON file with signatories')}
onChange={setShowSignaturesUpload}
value={showSignaturesUpload}
/>
</Modal.Columns>
{!showSignaturesUpload && (
<Modal.Columns
hint={
<>
<p>{t<string>('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<string>('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>
</>
}
>
<InputAddressMulti
available={availableSignatories}
availableLabel={t<string>('available signatories')}
help={t<string>('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<string>('selected signatories')}
/>
</Modal.Columns>
)}
{showSignaturesUpload && (
<Modal.Columns hint={t<string>('Supply a JSON file with the list of signatories.')}>
<InputFile
accept={acceptedFormats}
className='full'
clearContent={!uploadedSignatories.length && isUploadedFileValid}
help={t<string>('Select a JSON key file with the list of signatories.')}
isError={!isUploadedFileValid}
label={t<string>('upload signatories list')}
onChange={_onChangeFile}
withLabel
/>
{!!uploadedSignatories.length && (
<Labelled
label={t<string>('found signatories')}
labelExtra={(
<IconLink
icon='sync'
label={t<string>('Reset')}
onClick={resetFileUpload}
/>
)}
>
<div className='ui--Static ui dropdown selection'>
{uploadedSignatories.map((address): React.ReactNode => (
<div key={address}>
<AddressMini
value={address}
withSidebar={false}
/>
</div>
))}
</div>
</Labelled>
)}
{uploadedFileError && (
<MarkError content={uploadedFileError} />
)}
</Modal.Columns>
)}
<Modal.Columns hint={t<string>('The threshold for approval should be less or equal to the number of signatories for this multisig.')}>
<InputNumber
help={t<string>('The threshold for this multisig')}
Expand Down Expand Up @@ -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;
}
`);

0 comments on commit 989e698

Please sign in to comment.