Skip to content

Commit

Permalink
Merge pull request #5366 from LiskHQ/5340-continuous-flow-for-multisi…
Browse files Browse the repository at this point in the history
…g-transaction-signing

Continuous flow for multisig transaction signing
  • Loading branch information
ManuGowda authored Oct 12, 2023
2 parents bbdaf92 + 4f04ea3 commit 5a512e1
Show file tree
Hide file tree
Showing 20 changed files with 360 additions and 74 deletions.
2 changes: 2 additions & 0 deletions setup/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ module.exports = {
'src/modules/account/components/SwitchAccount/SwitchAccount.js',
'src/modules/account/components/RemoveSelectedAccountFlow/RemoveSelectedAccountFlow.js',
'src/modules/blockchainApplication/connection/components/RequestView/RequestView.js',
'src/modules/transaction/utils/multisignatureUtils.js',
'src/modules/transaction/components/Multisignature/Multisignature.js',
'src/modules/hardwareWallet/store/actions/devicesActions.js',
'/src/modules/hardwareWallet/store/reducers/devicesReducer.js',
'src/modules/pos/validator/components/ClaimRewardsView/index.js',
Expand Down
4 changes: 4 additions & 0 deletions src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"64 bytes left": "64 bytes left",
"A bit more. Make sure to type at least 3 characters.": "A bit more. Make sure to type at least 3 characters.",
"A new management feature allows you to seamlessly add and switch between applications. The dedicated application tab provides a comprehensive overview of registered, active, and terminated blockchain applications, and statistics.": "A new management feature allows you to seamlessly add and switch between applications. The dedicated application tab provides a comprehensive overview of registered, active, and terminated blockchain applications, and statistics.",
"A required signatory account": "A required signatory account",
"About": "About",
"Access your account by scanning the QR code below with the Lisk Mobile App:": "Access your account by scanning the QR code below with the Lisk Mobile App:",
"Access your old address": "Access your old address",
Expand Down Expand Up @@ -540,6 +541,7 @@
"Request token": "Request token",
"Request tokens": "Request tokens",
"Required": "Required",
"Required account": "Required account",
"Required signatures": "Required signatures",
"Requirements": "Requirements",
"Restart app": "Restart app",
Expand Down Expand Up @@ -632,6 +634,7 @@
"Successfully edited": "Successfully edited",
"Successfully paired with {{sanitizedName}}": "Successfully paired with {{sanitizedName}}",
"Supply": "Supply",
"Switch Account": "Switch Account",
"Switch account": "Switch account",
"Switch application": "Switch application",
"Switch device": "Switch device",
Expand Down Expand Up @@ -846,6 +849,7 @@
"more": "more",
"removed": "removed",
"to": "to",
"to complete this transaction has been found on your Lisk Desktop. Please click on “Switch account” to complete this transaction.": "to complete this transaction has been found on your Lisk Desktop. Please click on “Switch account” to complete this transaction.",
"value": "value",
"when the locking period ends.": "when the locking period ends.",
"will be available to unlock in": "will be available to unlock in",
Expand Down
4 changes: 4 additions & 0 deletions src/modules/account/hooks/useAccounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ export function useAccounts() {
const getAccountByAddress = (address) =>
accounts.find((account) => account.metadata.address === address);

const getAccountByPublicKey = (pubkey) =>
accounts.find((account) => account.metadata.pubkey === pubkey);

return {
accounts,
setAccount,
deleteAccountByAddress,
getAccountByPublicKey,
getAccountByAddress,
};
}
7 changes: 7 additions & 0 deletions src/modules/account/hooks/useAccounts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ describe('useAccount hook', () => {
expect(account).toMatchObject(mockSavedAccounts[0]);
});

it('Should return specific account selected by pubkey', async () => {
const { getAccountByPublicKey } = result.current;
const pubkey = mockSavedAccounts[0].metadata.pubkey;
const account = getAccountByPublicKey(pubkey);
expect(account).toMatchObject(mockSavedAccounts[0]);
});

it('deleteAccount should dispatch an action', async () => {
const { deleteAccountByAddress } = result.current;
const address = mockSavedAccounts[0].metadata.address;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import Box from 'src/theme/box';
import routes from 'src/routes/routes';
import BlockchainAppDetailsHeader from '@blockchainApplication/explore/components/BlockchainAppDetailsHeader';
import WarningNotification from '@common/components/warningNotification';
import { ReactComponent as SwtichIcon } from '../../../../../../setup/react/assets/images/icons/switch-icon.svg';
import { ReactComponent as SwitchIcon } from '../../../../../../setup/react/assets/images/icons/switch-icon.svg';
import EmptyState from './EmptyState';
import styles from './requestSummary.css';

Expand Down Expand Up @@ -231,7 +231,7 @@ const RequestSummary = ({ nextStep, history, message }) => {
{!isSenderCurrentAccount && !!encryptedSenderAccount && (
<TertiaryButton onClick={handleSwitchAccount}>
<span>{t('Switch to signing account')}</span>{' '}
<SwtichIcon data-testid="switch-icon" />
<SwitchIcon data-testid="switch-icon" />
</TertiaryButton>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/modules/common/components/OldMultiStep/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class MultiStep extends React.Component {

reset() {
this.prev({ reset: true });
this.props.onChange?.();
this.props.onChange?.(this.state);
}

render() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const UploadJSONInput = ({
label,
prefixLabel,
placeholderText,
isDisabled = false,
}) => {
const { t } = useTranslation();

Expand Down Expand Up @@ -43,6 +44,7 @@ const UploadJSONInput = ({
'application/JSON': ['.json'],
},
noClick: true,
disabled: isDisabled,
});

return (
Expand All @@ -52,6 +54,7 @@ const UploadJSONInput = ({
<label className={styles.fileInputBtn}>
{label}
<input
disabled={isDisabled}
role="button"
className={`${styles.input} clickableFileInput`}
type="file"
Expand All @@ -67,6 +70,7 @@ const UploadJSONInput = ({
}`}
>
<textarea
disabled={isDisabled}
onPaste={onPaste}
onChange={onPaste}
value={value && !error ? JSON.stringify(value) : ''}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
}

& > h6 {
font-weight: var(--font-weight-semi-bold);
font-weight: var(--font-weight-very-bold);
font-size: 28px;
margin: 20px 0 0;
color: var(--color-maastricht-blue);
}
Expand All @@ -41,9 +42,24 @@
}

& > img {
width: 210px;
width: 120px;
margin-top: var(--horizontal-padding-l);
}

& .requiredAccountSection {
margin-top: 14px;
box-sizing: border-box;
width: 100%;
padding: 0 24px;
display: flex;
flex-direction: column;

& .requiredAccountTitle {
margin: 24px 0 8px 0;
font-size: 14px;
color: var(--color-slate-gray);
}
}
}

.primaryActions {
Expand All @@ -55,13 +71,22 @@
align-items: center;
margin-top: 20px;

& .buttonContent {
display: flex;
align-items: center;
justify-content: center;
}

& .copy,
& .switchAccountBtn,
& .download {
width: 160px;
width: 184px;
flex-shrink: 0;
margin: 0 10px;
padding: 0 22px;

& img {
& img,
& svg {
vertical-align: middle;
margin-right: 10px;
}
Expand Down
141 changes: 112 additions & 29 deletions src/modules/transaction/components/Multisignature/Multisignature.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { PrimaryButton, SecondaryButton } from 'src/theme/buttons';
import { cryptography } from '@liskhq/lisk-client';
import Illustration from 'src/modules/common/components/illustration';
Expand All @@ -7,20 +7,64 @@ import { txStatusTypes } from '@transaction/configuration/txStatus';
import { getErrorReportMailto } from 'src/utils/helpers';

import copyToClipboard from 'copy-to-clipboard';
import { toTransactionJSON, downloadJSON, joinModuleAndCommand } from '@transaction/utils';
import { downloadJSON, joinModuleAndCommand, toTransactionJSON } from '@transaction/utils';
import { MODULE_COMMANDS_NAME_MAP } from '@transaction/configuration/moduleCommand';
import Icon from 'src/theme/Icon';
import { useAccounts, useCurrentAccount } from '@account/hooks';
import { truncateAddress } from '@wallet/utils/account';
import { ReactComponent as SwitchIcon } from '@setup/react/assets/images/icons/switch-icon.svg';
import WarningNotification from '@common/components/warningNotification';
import AccountRow from '@account/components/AccountRow';
import classNames from 'classnames';
import { getNextAccountToSign } from '@transaction/utils/multisignatureUtils';
import getIllustration from '../TxBroadcaster/illustrationsMap';
import styles from './Multisignature.css';
import useTxInitiatorAccount from '../../hooks/useTxInitiatorAccount';

export const PartiallySignedActions = ({ onDownload, t }) => (
<PrimaryButton className={`${styles.download} download-button`} onClick={onDownload}>
<span className={styles.buttonContent}>
<Icon name="download" />
{t('Download')}
</span>
</PrimaryButton>
);
export const PartiallySignedActions = ({
onDownload,
nextAccountToSign,
t,
transactionJSON,
reset,
}) => {
const [, setCurrentAccount] = useCurrentAccount();

const handleSwitchAccount = () => {
const stringifiedTransaction = encodeURIComponent(JSON.stringify(transactionJSON));
setCurrentAccount(
nextAccountToSign,
`/wallet?modal=signMultiSignTransaction&stringifiedTransaction=${stringifiedTransaction}`,
true,
{ stringifiedTransaction }
);
reset?.();
};

const DownloadButton = nextAccountToSign ? SecondaryButton : PrimaryButton;

return (
<>
<DownloadButton className={`${styles.download} download-button`} onClick={onDownload}>
<span className={styles.buttonContent}>
{nextAccountToSign ? <Icon name="downloadBlue" /> : <Icon name="download" />}
{t('Download')}
</span>
</DownloadButton>
{nextAccountToSign && (
<PrimaryButton
className={`${styles.switchAccountBtn} download-button`}
onClick={handleSwitchAccount}
>
<span className={styles.buttonContent}>
<SwitchIcon />
{t('Switch Account')}
</span>
</PrimaryButton>
)}
</>
);
};

export const FullySignedActions = ({ t, onDownload, onSend }) => (
<>
Expand All @@ -39,15 +83,15 @@ export const FullySignedActions = ({ t, onDownload, onSend }) => (
</>
);

const ErrorActions = ({ t, status, message, network, application }) => (
export const ErrorActions = ({ t, status, message, network, application }) => (
<a
className="report-error-link"
href={getErrorReportMailto({
error: status?.message,
errorMessage: message,
networkIdentifier: network.networkIdentifier,
serviceUrl: network.serviceUrl,
liskCoreVersion: network.networkVersion,
networkIdentifier: network?.networkIdentifier,
serviceUrl: network?.serviceUrl,
liskCoreVersion: network?.networkVersion,
application,
})}
target="_top"
Expand All @@ -72,13 +116,25 @@ const Multisignature = ({
network,
moduleCommandSchemas,
application,
reset,
}) => {
const [copied, setCopied] = useState(false);
const ref = useRef();
const { getAccountByPublicKey } = useAccounts();
const moduleCommand = joinModuleAndCommand(transactions.signedTransaction);
const paramSchema = moduleCommandSchemas[moduleCommand];
const transactionJSON = toTransactionJSON(transactions.signedTransaction, paramSchema);

const { txInitiatorAccount } = useTxInitiatorAccount({
senderPublicKey: transactionJSON.senderPublicKey,
});

const nextAccountToSign = getNextAccountToSign({
getAccountByPublicKey,
transactionJSON,
txInitiatorAccount,
});

const onCopy = () => {
setCopied(true);
copyToClipboard(JSON.stringify(transactionJSON));
Expand Down Expand Up @@ -113,8 +169,28 @@ const Multisignature = ({
<div className={`${styles.wrapper} ${className}`}>
<Illustration name={getIllustration(status.code, 'signMultisignature')} />
<h6 className="result-box-header">{title}</h6>
<p className="transaction-status body-message">{message}</p>

{!nextAccountToSign && <p className="transaction-status body-message">{message}</p>}
{nextAccountToSign && status.code !== txStatusTypes.multisigSignatureSuccess && (
<div className={styles.requiredAccountSection}>
<WarningNotification
isVisible
message={
<span>
{t('A required signatory account')}{' '}
<b>
({nextAccountToSign?.metadata?.name} -{' '}
{truncateAddress(nextAccountToSign?.metadata?.address)})
</b>{' '}
{t(
'to complete this transaction has been found on your Lisk Desktop. Please click on “Switch account” to complete this transaction.'
)}
</span>
}
/>
<h4 className={styles.requiredAccountTitle}>{t('Required account')}</h4>
<AccountRow className={classNames(styles.accountRow)} account={nextAccountToSign} />
</div>
)}
<div className={styles.primaryActions}>
{status.code === txStatusTypes.broadcastSuccess && !noBackButton ? (
<PrimaryButton
Expand All @@ -134,20 +210,27 @@ const Multisignature = ({
/>
) : null}
{status.code !== txStatusTypes.broadcastSuccess &&
status.code !== txStatusTypes.broadcastError ? (
<SecondaryButton className={`${styles.copy} copy-button`} onClick={onCopy}>
<span className={styles.buttonContent}>
<Icon name={copied ? 'transactionStatusSuccessful' : 'copy'} />
{t(copied ? 'Copied' : 'Copy')}
</span>
</SecondaryButton>
) : null}
{status.code === txStatusTypes.multisigSignatureSuccess ? (
status.code !== txStatusTypes.broadcastError &&
!nextAccountToSign && (
<SecondaryButton className={`${styles.copy} copy-button`} onClick={onCopy}>
<span className={styles.buttonContent}>
<Icon name={copied ? 'transactionStatusSuccessful' : 'copy'} />
{t(copied ? 'Copied' : 'Copy')}
</span>
</SecondaryButton>
)}
{status.code === txStatusTypes.multisigSignatureSuccess && (
<FullySignedActions onDownload={onDownload} t={t} onSend={onSend} />
) : null}
{status.code === txStatusTypes.multisigSignaturePartialSuccess ? (
<PartiallySignedActions onDownload={onDownload} t={t} />
) : null}
)}
{status.code === txStatusTypes.multisigSignaturePartialSuccess && (
<PartiallySignedActions
onDownload={onDownload}
nextAccountToSign={nextAccountToSign}
t={t}
transactionJSON={transactionJSON}
reset={reset}
/>
)}
</div>
</div>
);
Expand Down
Loading

0 comments on commit 5a512e1

Please sign in to comment.