From 31d26d8e203e52802d9288037978e09e0bf4b558 Mon Sep 17 00:00:00 2001 From: "Thang X. Vu" Date: Mon, 3 Apr 2023 15:54:22 +0700 Subject: [PATCH 1/8] feat: handle sign raw data request (#8) --- packages/base/package.json | 1 + packages/base/src/requests/WalletState.ts | 26 ++++++++++++++++++++++- yarn.lock | 1 + 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/base/package.json b/packages/base/package.json index 968f029b..55e4c99c 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -20,6 +20,7 @@ "@coong/utils": "^0.0.20", "@polkadot/networks": "^10.4.1", "@polkadot/types": "^9.14.1", + "@polkadot/util": "^10.4.1", "@polkadot/util-crypto": "^10.4.1", "rxjs": "^7.8.0" }, diff --git a/packages/base/src/requests/WalletState.ts b/packages/base/src/requests/WalletState.ts index 2bfa7bc6..59c31b81 100644 --- a/packages/base/src/requests/WalletState.ts +++ b/packages/base/src/requests/WalletState.ts @@ -1,6 +1,7 @@ import { InjectedAccount } from '@polkadot/extension-inject/types'; import { TypeRegistry } from '@polkadot/types'; -import { SignerPayloadJSON } from '@polkadot/types/types'; +import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types'; +import { u8aToHex, u8aWrapBytes } from '@polkadot/util'; import { encodeAddress } from '@polkadot/util-crypto'; import { KeypairType } from '@polkadot/util-crypto/types'; import Keyring from '@coong/keyring'; @@ -170,7 +171,30 @@ export default class WalletState { cancelSignExtrinsic() { const currentMessage = this.getCurrentRequestMessage('tab/signExtrinsic'); + currentMessage.reject(new StandardCoongError('Cancelled')); + } + + async signRawMessage(password: string) { + await this.#keyring.verifyPassword(password); + + const currentMessage = this.getCurrentRequestMessage('tab/signRaw'); + + const { id, request, resolve } = currentMessage; + const payloadJSON = request.body as SignerPayloadRaw; + + const pair = this.#keyring.getSigningPair(payloadJSON.address); + pair.unlock(password); + + const signature = u8aToHex(pair.sign(u8aWrapBytes(payloadJSON.data))); + + resolve({ + id, + signature, + }); + } + cancelSignRawMessage() { + const currentMessage = this.getCurrentRequestMessage('tab/signRaw'); currentMessage.reject(new StandardCoongError('Cancelled')); } diff --git a/yarn.lock b/yarn.lock index 9f593612..0e224f07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -375,6 +375,7 @@ __metadata: "@polkadot/networks": ^10.4.1 "@polkadot/types": ^9.14.1 "@polkadot/ui-keyring": ^2.11.2 + "@polkadot/util": ^10.4.1 "@polkadot/util-crypto": ^10.4.1 rxjs: ^7.8.0 languageName: unknown From 440d4b1436434c73ff91ffce09371e1316e626d3 Mon Sep 17 00:00:00 2001 From: "Thang X. Vu" Date: Mon, 3 Apr 2023 16:08:30 +0700 Subject: [PATCH 2/8] feat: add sign raw message ui, reorganize components (#8) --- .../pages/Request/RequestContent.tsx | 5 +- .../Request/RequestSigning/DetailRow.tsx | 39 ++++++++ .../Request/RequestSigning/RequestDetails.tsx | 19 ++++ .../RequestSigning/RequestSignRawMessage.tsx | 36 +++++++ .../RequestTransactionApproval.tsx | 41 ++++++++ .../pages/Request/RequestSigning/SignArea.tsx | 58 +++++++++++ .../RequestTransactionApproval.spec.tsx | 0 .../hooks/useRawMessageDetails.ts | 28 ++++++ .../RequestSigning/hooks/useTargetAccount.ts | 35 +++++++ .../hooks/useTransactionDetails.ts | 66 +++++++++++++ .../RequestDetails.tsx | 84 ---------------- .../RequestTransactionApproval/index.tsx | 95 ------------------- 12 files changed, 326 insertions(+), 180 deletions(-) create mode 100644 packages/ui/src/components/pages/Request/RequestSigning/DetailRow.tsx create mode 100644 packages/ui/src/components/pages/Request/RequestSigning/RequestDetails.tsx create mode 100644 packages/ui/src/components/pages/Request/RequestSigning/RequestSignRawMessage.tsx create mode 100644 packages/ui/src/components/pages/Request/RequestSigning/RequestTransactionApproval.tsx create mode 100644 packages/ui/src/components/pages/Request/RequestSigning/SignArea.tsx rename packages/ui/src/components/pages/Request/{RequestTransactionApproval => RequestSigning}/__tests__/RequestTransactionApproval.spec.tsx (100%) create mode 100644 packages/ui/src/components/pages/Request/RequestSigning/hooks/useRawMessageDetails.ts create mode 100644 packages/ui/src/components/pages/Request/RequestSigning/hooks/useTargetAccount.ts create mode 100644 packages/ui/src/components/pages/Request/RequestSigning/hooks/useTransactionDetails.ts delete mode 100644 packages/ui/src/components/pages/Request/RequestTransactionApproval/RequestDetails.tsx delete mode 100644 packages/ui/src/components/pages/Request/RequestTransactionApproval/index.tsx diff --git a/packages/ui/src/components/pages/Request/RequestContent.tsx b/packages/ui/src/components/pages/Request/RequestContent.tsx index 49810cfd..3c409c4c 100644 --- a/packages/ui/src/components/pages/Request/RequestContent.tsx +++ b/packages/ui/src/components/pages/Request/RequestContent.tsx @@ -2,7 +2,8 @@ import React, { FC } from 'react'; import { CoongError, ErrorCode } from '@coong/utils'; import { CircularProgress } from '@mui/material'; import RequestAccess from 'components/pages/Request/RequestAccess'; -import RequestTransactionApproval from 'components/pages/Request/RequestTransactionApproval'; +import RequestSignRawMessage from 'components/pages/Request/RequestSigning/RequestSignRawMessage'; +import RequestTransactionApproval from 'components/pages/Request/RequestSigning/RequestTransactionApproval'; import useCurrentRequestMessage from 'hooks/messages/useCurrentRequestMessage'; import { Props } from 'types'; @@ -23,6 +24,8 @@ const RequestContent: FC = () => { return ; } else if (requestName === 'tab/signExtrinsic') { return ; + } else if (requestName === 'tab/signRaw') { + return ; } throw new CoongError(ErrorCode.UnknownRequest); diff --git a/packages/ui/src/components/pages/Request/RequestSigning/DetailRow.tsx b/packages/ui/src/components/pages/Request/RequestSigning/DetailRow.tsx new file mode 100644 index 00000000..eca67d44 --- /dev/null +++ b/packages/ui/src/components/pages/Request/RequestSigning/DetailRow.tsx @@ -0,0 +1,39 @@ +import { FC } from 'react'; +import clsx from 'clsx'; +import { Props } from 'types'; + +export enum ValueStyle { + TEXT_BOLD, + BOX, +} + +export interface DetailRowProps extends Props { + name: string; + value: any; + breakWord?: boolean; + style?: ValueStyle; +} + +const DetailRow: FC = ({ name, value, breakWord = false, style = ValueStyle.TEXT_BOLD }) => { + const renderValue = () => { + switch (style) { + case ValueStyle.BOX: + return ( +
+ {value} +
+ ); + case ValueStyle.TEXT_BOLD: + default: + return {value}; + } + }; + + return ( +
+
{name}:
+ {renderValue()} +
+ ); +}; +export default DetailRow; diff --git a/packages/ui/src/components/pages/Request/RequestSigning/RequestDetails.tsx b/packages/ui/src/components/pages/Request/RequestSigning/RequestDetails.tsx new file mode 100644 index 00000000..c0e01b1f --- /dev/null +++ b/packages/ui/src/components/pages/Request/RequestSigning/RequestDetails.tsx @@ -0,0 +1,19 @@ +import { FC } from 'react'; +import DetailRow, { DetailRowProps } from 'components/pages/Request/RequestSigning/DetailRow'; +import { Props } from 'types'; + +interface RequestDetails extends Props { + rows: DetailRowProps[]; +} + +const RequestDetails: FC = ({ className = '', rows }) => { + return ( +
+ {rows.map((row) => ( + + ))} +
+ ); +}; + +export default RequestDetails; diff --git a/packages/ui/src/components/pages/Request/RequestSigning/RequestSignRawMessage.tsx b/packages/ui/src/components/pages/Request/RequestSigning/RequestSignRawMessage.tsx new file mode 100644 index 00000000..017b42c2 --- /dev/null +++ b/packages/ui/src/components/pages/Request/RequestSigning/RequestSignRawMessage.tsx @@ -0,0 +1,36 @@ +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import AccountCard from 'components/pages/Accounts/AccountCard'; +import RequestDetails from 'components/pages/Request/RequestSigning/RequestDetails'; +import SignArea from 'components/pages/Request/RequestSigning/SignArea'; +import { useRawMessageDetails } from 'components/pages/Request/RequestSigning/hooks/useRawMessageDetails'; +import useTargetAccount from 'components/pages/Request/RequestSigning/hooks/useTargetAccount'; +import { RequestProps } from 'components/pages/Request/types'; +import { useWalletState } from 'providers/WalletStateProvider'; + +const RequestSignRawMessage: FC = ({ className = '', message }) => { + const { t } = useTranslation(); + const { walletState } = useWalletState(); + const targetAccount = useTargetAccount(message); + const detailRows = useRawMessageDetails(message); + + const doSignMessage = async (password: string) => { + await walletState.signRawMessage(password); + }; + + const cancelRequest = () => { + walletState.cancelSignRawMessage(); + }; + + return ( +
+

{t('Sign Message Request')}

+

{t('You are signing a message with account')}

+ {targetAccount && } + + +
+ ); +}; + +export default RequestSignRawMessage; diff --git a/packages/ui/src/components/pages/Request/RequestSigning/RequestTransactionApproval.tsx b/packages/ui/src/components/pages/Request/RequestSigning/RequestTransactionApproval.tsx new file mode 100644 index 00000000..7754ae9b --- /dev/null +++ b/packages/ui/src/components/pages/Request/RequestSigning/RequestTransactionApproval.tsx @@ -0,0 +1,41 @@ +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import AccountCard from 'components/pages/Accounts/AccountCard'; +import RequestDetails from 'components/pages/Request/RequestSigning/RequestDetails'; +import SignArea from 'components/pages/Request/RequestSigning/SignArea'; +import useTargetAccount from 'components/pages/Request/RequestSigning/hooks/useTargetAccount'; +import useTransactionDetails from 'components/pages/Request/RequestSigning/hooks/useTransactionDetails'; +import { RequestProps } from 'components/pages/Request/types'; +import { useWalletState } from 'providers/WalletStateProvider'; + +const RequestTransactionApproval: FC = ({ className, message }) => { + const { t } = useTranslation(); + const { walletState } = useWalletState(); + const targetAccount = useTargetAccount(message); + const detailRows = useTransactionDetails(message); + + const approveTransaction = async (password: string) => { + // signing & accounts decryption are synchronous operations + // and might take some time to do + // so we delay it a short amount of time to make sure the UI could be updated (disable button, ...) + // before the signing process begin + // TODO: Moving CPU-intensive operations to worker + await walletState.approveSignExtrinsic(password); + }; + + const cancelRequest = () => { + walletState.cancelSignExtrinsic(); + }; + + return ( +
+

{t('Transaction Approval Request')}

+

{t('You are approving a transaction with account')}

+ {targetAccount && } + + +
+ ); +}; + +export default RequestTransactionApproval; diff --git a/packages/ui/src/components/pages/Request/RequestSigning/SignArea.tsx b/packages/ui/src/components/pages/Request/RequestSigning/SignArea.tsx new file mode 100644 index 00000000..63d24e95 --- /dev/null +++ b/packages/ui/src/components/pages/Request/RequestSigning/SignArea.tsx @@ -0,0 +1,58 @@ +import { FC, FormEvent, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Form } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { useToggle } from 'react-use'; +import { Button, TextField } from '@mui/material'; +import { Props } from 'types'; + +interface SignAreaProps extends Props { + onSign: (password: string) => void; + onCancel: () => void; + cancelButtonLabel?: string; + signButtonLabel?: string; +} + +const SignArea: FC = ({ onSign, onCancel, cancelButtonLabel = 'Cancel', signButtonLabel = 'Sign' }) => { + const { t } = useTranslation(); + const [password, setPassword] = useState(''); + const [loading, toggleLoading] = useToggle(false); + + const doSign = (e: FormEvent) => { + e.preventDefault(); + toggleLoading(true); + + setTimeout(async () => { + try { + await onSign(password); + } catch (e: any) { + toggleLoading(false); + toast.error(t(e.message)); + } + }, 200); + }; + + return ( +
+ ('Wallet password')} + size='medium' + type='password' + fullWidth + autoFocus + value={password} + onChange={(event) => setPassword(event.target.value)} + /> +
+ + +
+ + ); +}; + +export default SignArea; diff --git a/packages/ui/src/components/pages/Request/RequestTransactionApproval/__tests__/RequestTransactionApproval.spec.tsx b/packages/ui/src/components/pages/Request/RequestSigning/__tests__/RequestTransactionApproval.spec.tsx similarity index 100% rename from packages/ui/src/components/pages/Request/RequestTransactionApproval/__tests__/RequestTransactionApproval.spec.tsx rename to packages/ui/src/components/pages/Request/RequestSigning/__tests__/RequestTransactionApproval.spec.tsx diff --git a/packages/ui/src/components/pages/Request/RequestSigning/hooks/useRawMessageDetails.ts b/packages/ui/src/components/pages/Request/RequestSigning/hooks/useRawMessageDetails.ts new file mode 100644 index 00000000..fb0d98a5 --- /dev/null +++ b/packages/ui/src/components/pages/Request/RequestSigning/hooks/useRawMessageDetails.ts @@ -0,0 +1,28 @@ +import { useMemo } from 'react'; +import { SignerPayloadRaw } from '@polkadot/types/types'; +import { isAscii, u8aToString, u8aUnwrapBytes } from '@polkadot/util'; +import { WalletRequestWithResolver } from '@coong/base/types'; +import { DetailRowProps, ValueStyle } from 'components/pages/Request/RequestSigning/DetailRow'; + +export function useRawMessageDetails(message: WalletRequestWithResolver): DetailRowProps[] { + const { origin, request } = message; + const payloadJSON = request.body as SignerPayloadRaw; + const { data } = payloadJSON; + + return useMemo( + () => [ + { + name: 'from', + value: origin, + breakWord: true, + }, + { + name: 'bytes', + value: isAscii(data) ? u8aToString(u8aUnwrapBytes(data)) : data, + breakWord: true, + style: ValueStyle.BOX, + }, + ], + [message], + ); +} diff --git a/packages/ui/src/components/pages/Request/RequestSigning/hooks/useTargetAccount.ts b/packages/ui/src/components/pages/Request/RequestSigning/hooks/useTargetAccount.ts new file mode 100644 index 00000000..5ddf9579 --- /dev/null +++ b/packages/ui/src/components/pages/Request/RequestSigning/hooks/useTargetAccount.ts @@ -0,0 +1,35 @@ +import { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useAsync } from 'react-use'; +import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types'; +import { encodeAddress } from '@polkadot/util-crypto'; +import { WalletRequestWithResolver } from '@coong/base/types'; +import useThrowError from 'hooks/useThrowError'; +import { useWalletState } from 'providers/WalletStateProvider'; +import { RootState } from 'redux/store'; +import { AccountInfoExt } from 'types'; + +export default function useTargetAccount(message: WalletRequestWithResolver) { + const { keyring } = useWalletState(); + const { addressPrefix } = useSelector((state: RootState) => state.app); + const [targetAccount, setTargetAccount] = useState(); + const throwError = useThrowError(); + const { request } = message; + + useAsync(async () => { + try { + const payloadJSON = request.body as SignerPayloadJSON | SignerPayloadRaw; + const account = await keyring.getAccount(payloadJSON.address); + const networkAddress = encodeAddress(account.address, addressPrefix); + + setTargetAccount({ + ...account, + networkAddress, + }); + } catch (e: any) { + throwError(e); + } + }, [message]); + + return targetAccount; +} diff --git a/packages/ui/src/components/pages/Request/RequestSigning/hooks/useTransactionDetails.ts b/packages/ui/src/components/pages/Request/RequestSigning/hooks/useTransactionDetails.ts new file mode 100644 index 00000000..e70eb6d4 --- /dev/null +++ b/packages/ui/src/components/pages/Request/RequestSigning/hooks/useTransactionDetails.ts @@ -0,0 +1,66 @@ +import { useMemo } from 'react'; +import { TypeRegistry } from '@polkadot/types'; +import { ExtrinsicEra } from '@polkadot/types/interfaces'; +import { SignerPayloadJSON } from '@polkadot/types/types'; +import { bnToBn, formatNumber, hexToNumber } from '@polkadot/util'; +import { WalletRequestWithResolver } from '@coong/base/types'; +import { DetailRowProps } from 'components/pages/Request/RequestSigning/DetailRow'; + +function mortalityAsString(era: ExtrinsicEra, hexBlockNumber: string): string { + if (era.isImmortalEra) { + return 'Immortal'; + } + + const blockNumber = bnToBn(hexBlockNumber); + const mortal = era.asMortalEra; + const birth = formatNumber(mortal.birth(blockNumber)); + const death = formatNumber(mortal.death(blockNumber)); + + return `Mortal, valid from ${birth} to ${death}`; +} + +const registry = new TypeRegistry(); + +export default function useTransactionDetails(message: WalletRequestWithResolver): DetailRowProps[] { + const { origin, request } = message; + + const payloadJSON = request.body as SignerPayloadJSON; + + const { genesisHash, specVersion, nonce, method, blockNumber } = payloadJSON; + + registry.setSignedExtensions(payloadJSON.signedExtensions); + const { era } = registry.createType('ExtrinsicPayload', payloadJSON, { version: payloadJSON.version }); + + return useMemo( + () => [ + { + name: 'from', + value: origin, + breakWord: true, + }, + { + name: 'genesis', + value: genesisHash, + breakWord: true, + }, + { + name: 'version', + value: bnToBn(specVersion).toNumber(), + }, + { + name: 'nonce', + value: hexToNumber(nonce), + }, + { + name: 'method data', + value: method, + breakWord: true, + }, + { + name: 'life time', + value: mortalityAsString(era, blockNumber), + }, + ], + [message], + ); +} diff --git a/packages/ui/src/components/pages/Request/RequestTransactionApproval/RequestDetails.tsx b/packages/ui/src/components/pages/Request/RequestTransactionApproval/RequestDetails.tsx deleted file mode 100644 index a8e7dfe0..00000000 --- a/packages/ui/src/components/pages/Request/RequestTransactionApproval/RequestDetails.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { FC } from 'react'; -import { TypeRegistry } from '@polkadot/types'; -import { ExtrinsicEra } from '@polkadot/types/interfaces'; -import { SignerPayloadJSON } from '@polkadot/types/types'; -import { bnToBn, formatNumber, hexToNumber } from '@polkadot/util'; -import clsx from 'clsx'; -import { RequestProps } from 'components/pages/Request/types'; - -function mortalityAsString(era: ExtrinsicEra, hexBlockNumber: string): string { - if (era.isImmortalEra) { - return 'Immortal'; - } - - const blockNumber = bnToBn(hexBlockNumber); - const mortal = era.asMortalEra; - const birth = formatNumber(mortal.birth(blockNumber)); - const death = formatNumber(mortal.death(blockNumber)); - - return `Mortal, valid from ${birth} to ${death}`; -} - -const registry = new TypeRegistry(); - -interface DetailLine { - name: string; - value: any; - breakWord?: boolean; -} - -const RequestDetails: FC = ({ className, message }) => { - const { origin, request } = message; - - const payloadJSON = request.body as SignerPayloadJSON; - - const { genesisHash, specVersion, nonce, method, blockNumber } = payloadJSON; - - registry.setSignedExtensions(payloadJSON.signedExtensions); - const { era } = registry.createType('ExtrinsicPayload', payloadJSON, { version: payloadJSON.version }); - - const requestDetails: DetailLine[] = [ - { - name: 'from', - value: origin, - breakWord: true, - }, - { - name: 'genesis', - value: genesisHash, - breakWord: true, - }, - { - name: 'version', - value: bnToBn(specVersion).toNumber(), - }, - { - name: 'nonce', - value: hexToNumber(nonce), - }, - { - name: 'method data', - value: method, - breakWord: true, - }, - { - name: 'life time', - value: mortalityAsString(era, blockNumber), - }, - ]; - - return ( -
- {requestDetails.map(({ name, value, breakWord }) => ( -
-
{name}:
-
- {value} -
-
- ))} -
- ); -}; - -export default RequestDetails; diff --git a/packages/ui/src/components/pages/Request/RequestTransactionApproval/index.tsx b/packages/ui/src/components/pages/Request/RequestTransactionApproval/index.tsx deleted file mode 100644 index ba2fac59..00000000 --- a/packages/ui/src/components/pages/Request/RequestTransactionApproval/index.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { FC, FormEvent, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useSelector } from 'react-redux'; -import { Form } from 'react-router-dom'; -import { toast } from 'react-toastify'; -import { useAsync, useToggle } from 'react-use'; -import { SignerPayloadJSON } from '@polkadot/types/types'; -import { encodeAddress } from '@polkadot/util-crypto'; -import { Button, TextField } from '@mui/material'; -import AccountCard from 'components/pages/Accounts/AccountCard'; -import RequestDetails from 'components/pages/Request/RequestTransactionApproval/RequestDetails'; -import { RequestProps } from 'components/pages/Request/types'; -import useThrowError from 'hooks/useThrowError'; -import { useWalletState } from 'providers/WalletStateProvider'; -import { RootState } from 'redux/store'; -import { AccountInfoExt } from 'types'; - -const RequestTransactionApproval: FC = ({ className, message }) => { - const { keyring, walletState } = useWalletState(); - const { addressPrefix } = useSelector((state: RootState) => state.app); - const [password, setPassword] = useState(''); - const { request } = message; - const [loading, toggleLoading] = useToggle(false); - const [targetAccount, setTargetAccount] = useState(); - const throwError = useThrowError(); - const { t } = useTranslation(); - - useAsync(async () => { - try { - const payloadJSON = request.body as SignerPayloadJSON; - const account = await keyring.getAccount(payloadJSON.address); - const networkAddress = encodeAddress(account.address, addressPrefix); - - setTargetAccount({ - ...account, - networkAddress, - }); - } catch (e: any) { - throwError(e); - } - }, []); - - const approveTransaction = (e: FormEvent) => { - e.preventDefault(); - toggleLoading(true); - - // signing & accounts decryption are synchronous operations - // and might take some time to do - // so we delay it a short amount of time to make sure the UI could be updated (disable button, ...) - // before the signing process begin - // TODO: Moving CPU-intensive operations to worker - setTimeout(async () => { - try { - await walletState.approveSignExtrinsic(password); - } catch (e: any) { - toggleLoading(false); - toast.error(t(e.message)); - } - }, 200); - }; - - const cancelRequest = () => { - walletState.cancelSignExtrinsic(); - }; - - return ( -
-

Transaction Approval Request

-

You are approving a transaction with account

- {targetAccount && } - -
- setPassword(event.target.value)} - /> -
- - -
- -
- ); -}; - -export default RequestTransactionApproval; From 0635e9d6e5d1d8b685c6e736cbee4318bf2126ef Mon Sep 17 00:00:00 2001 From: "Thang X. Vu" Date: Mon, 3 Apr 2023 16:25:15 +0700 Subject: [PATCH 3/8] chore: add translation keys (#8) --- packages/ui/public/locales/en/translation.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/ui/public/locales/en/translation.json b/packages/ui/public/locales/en/translation.json index 9ea1633d..266ed962 100644 --- a/packages/ui/public/locales/en/translation.json +++ b/packages/ui/public/locales/en/translation.json @@ -77,5 +77,19 @@ "AccountNameRequired": "Account name is required", "AccountNameUsed": "Account name is already picked", "InternalError": "Internal error", - "Invalid request": "" + "Invalid request": "", + "Sign Message Request": "", + "You are signing a message with account": "", + "Sign Message": "", + "Transaction Approval Request": "", + "You are approving a transaction with account": "", + "Approve Transaction": "", + "from": "", + "bytes": "", + "genesis": "", + "version": "", + "nonce": "", + "method data": "", + "life time": "", + "Sign": "" } From 0a475b1b199941ebb51832578c5156bb8de4dcc4 Mon Sep 17 00:00:00 2001 From: "Thang X. Vu" Date: Mon, 3 Apr 2023 16:26:15 +0700 Subject: [PATCH 4/8] chore: short translation keys alphabetically (#8) --- .../ui/public/locales/en/translation.json | 146 +++++++++--------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/packages/ui/public/locales/en/translation.json b/packages/ui/public/locales/en/translation.json index 266ed962..c823eec3 100644 --- a/packages/ui/public/locales/en/translation.json +++ b/packages/ui/public/locales/en/translation.json @@ -1,95 +1,95 @@ { - "Welcome to Coong": "", - "Welcome to Coong Wallet!": "", - "This page should be loaded inside an iframe!": "", - "If you open this page by accident, it's safe to close it now.": "", "A multichain crypto wallet for Polkadot & Kusama ecosystem": "A multichain crypto wallet
for Polkadot & Kusama ecosystem", - "Set up your Coong wallet now": "", - "Create New Wallet": "", - "Restore existing wallet": "", - "Coming soon": "", - "First, choose your wallet password": "", - "Welcome back": "", - "Your password will be used to encrypt accounts as well as unlock the wallet, make sure to pick a strong & easy-to-remember password": "Your password will be used to encrypt accounts as well as unlock the wallet, make sure to pick a strong & easy-to-remember password.", - "Unlock your wallet": "", - "Wallet password": "", - "Next": "", - "Next, confirm your wallet password": "", - "Confirm wallet password": "", - "Unlock": "", - "Write down the below 12 words and keep it in a safe place.": "", - "Back": "", - "Finish": "", - "I have backed up my recovery phrase": "", - "Finally, back up your secret recovery phrase": "", + "AccountNameRequired": "Account name is required", + "AccountNameUsed": "Account name is already picked", + "AccountNotFound": "Account not found", "Accounts": "", - "Search by name": "", + "Address copied!": "", "Address format": "", - "New Account": "", - "Create new account": "", + "An application, self-identifying as request app name is requesting access your wallet from origin.": "An application, self-identifying as {{appName}} is requesting access your wallet from {{origin}}.", + "Approve Transaction": "", + "Back": "", + "Cancel": "", "Choose a name for your new account": "", - "New account name": "", + "Click to copy address": "", + "Close settings": "", + "Coming soon": "", + "Confirm wallet password": "", + "Connect": "", "Create": "", - "Cancel": "", - "My first account": "", - "Settings": "", - "Theme Mode": "", - "Language": "", + "Create New Wallet": "", + "Create new account": "", + "Create your first account now!": "", "Dark": "", - "System": "", + "Deselect all": "", + "Finally, back up your secret recovery phrase": "", + "Finish": "", + "First, choose your wallet password": "", + "I have backed up my recovery phrase": "", + "If you open this page by accident, it's safe to close it now.": "", + "InternalError": "Internal error", + "Invalid Request": "", + "Invalid request": "", + "InvalidMessageFormat": "Invalid message format", + "KeypairNotFound": "Keypair not found", + "KeyringLocked": "The keyring is locked, please unlock the wallet first", + "KeyringNotInitialized": "Keyring is not initialized", + "Language": "", "Light": "", "Lock the wallet": "", + "My first account": "", + "New Account": "", + "New account name": "", + "Next": "", + "Next, confirm your wallet password": "", + "No accounts found in wallet": "", + "No accounts meet search query:": "", + "No accounts selected": "", + "Only connect if you trust the application": "", "Open settings": "", - "Close settings": "", + "Password does not match": "", + "Password's too short": "", + "PasswordIncorrect": "Password incorrect", + "PasswordRequired": "Password required", "Reset wallet": "", - "Wallet Access Request": "", - "An application, self-identifying as request app name is requesting access your wallet from origin.": "An application, self-identifying as {{appName}} is requesting access your wallet from {{origin}}.", - "Setup your Coong wallet now to connect": "", + "Restore existing wallet": "", + "Search by name": "", + "Select all": "", "Select the accounts you'd like to connect": "", - "Only connect if you trust the application": "", - "Connect": "", - "Set up wallet": "", "Set up new wallet": "", - "Password's too short": "", - "Type again your chosen password to ensure you remember it.": "", - "Password does not match": "", - "Select all": "", - "Deselect all": "", - "No accounts selected": "", - "account(s) selected": "", - "Invalid Request": "", + "Set up wallet": "", + "Set up your Coong wallet now": "", + "Settings": "", + "Setup your Coong wallet now to connect": "", + "Sign": "", + "Sign Message": "", + "Sign Message Request": "", + "System": "", + "Theme Mode": "", + "This page should be loaded inside an iframe!": "", "This page should not be open directly!": "", - "No accounts meet search query:": "", - "No accounts found in wallet": "", - "Create your first account now!": "", - "Click to copy address": "", - "Address copied!": "", + "Transaction Approval Request": "", + "Type again your chosen password to ensure you remember it.": "", "UnknownRequest": "Unknown request", "UnknownRequestOrigin": "Unknown request origin", - "InvalidMessageFormat": "Invalid message format", - "KeypairNotFound": "Keypair not found", - "AccountNotFound": "Account not found", - "KeyringNotInitialized": "Keyring is not initialized", - "PasswordIncorrect": "Password incorrect", - "PasswordRequired": "Password required", + "Unlock": "", + "Unlock your wallet": "", + "Wallet Access Request": "", + "Wallet password": "", "WalletLocked": "The wallet is locked, please unlock it first", - "KeyringLocked": "The keyring is locked, please unlock the wallet first", - "AccountNameRequired": "Account name is required", - "AccountNameUsed": "Account name is already picked", - "InternalError": "Internal error", - "Invalid request": "", - "Sign Message Request": "", - "You are signing a message with account": "", - "Sign Message": "", - "Transaction Approval Request": "", + "Welcome back": "", + "Welcome to Coong": "", + "Welcome to Coong Wallet!": "", + "Write down the below 12 words and keep it in a safe place.": "", "You are approving a transaction with account": "", - "Approve Transaction": "", - "from": "", + "You are signing a message with account": "", + "Your password will be used to encrypt accounts as well as unlock the wallet, make sure to pick a strong & easy-to-remember password": "Your password will be used to encrypt accounts as well as unlock the wallet, make sure to pick a strong & easy-to-remember password.", + "account(s) selected": "", "bytes": "", + "from": "", "genesis": "", - "version": "", - "nonce": "", - "method data": "", "life time": "", - "Sign": "" + "method data": "", + "nonce": "", + "version": "" } From c377b8fc95d830c440be86b4c3d3a15b606dd0a1 Mon Sep 17 00:00:00 2001 From: "Thang X. Vu" Date: Mon, 3 Apr 2023 16:27:41 +0700 Subject: [PATCH 5/8] refactor: translate key for DetailRow (#8) --- .../src/components/pages/Request/RequestSigning/DetailRow.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/pages/Request/RequestSigning/DetailRow.tsx b/packages/ui/src/components/pages/Request/RequestSigning/DetailRow.tsx index eca67d44..5ff3c955 100644 --- a/packages/ui/src/components/pages/Request/RequestSigning/DetailRow.tsx +++ b/packages/ui/src/components/pages/Request/RequestSigning/DetailRow.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; import clsx from 'clsx'; import { Props } from 'types'; @@ -15,6 +16,7 @@ export interface DetailRowProps extends Props { } const DetailRow: FC = ({ name, value, breakWord = false, style = ValueStyle.TEXT_BOLD }) => { + const { t } = useTranslation(); const renderValue = () => { switch (style) { case ValueStyle.BOX: @@ -31,7 +33,7 @@ const DetailRow: FC = ({ name, value, breakWord = false, style = return (
-
{name}:
+
{t(name)}:
{renderValue()}
); From ed8895aa4f2acc0d943e28c3a1c72f9b75a91f2f Mon Sep 17 00:00:00 2001 From: "Thang X. Vu" Date: Thu, 6 Apr 2023 15:52:28 +0700 Subject: [PATCH 6/8] chore: add references to equivalent parts on polkadot extension repo (#8) --- packages/base/src/requests/WalletState.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/base/src/requests/WalletState.ts b/packages/base/src/requests/WalletState.ts index 59c31b81..0eadee80 100644 --- a/packages/base/src/requests/WalletState.ts +++ b/packages/base/src/requests/WalletState.ts @@ -160,6 +160,8 @@ export default class WalletState { const registry = new TypeRegistry(); registry.setSignedExtensions(payloadJSON.signedExtensions); + + // https://github.com/polkadot-js/extension/blob/master/packages/extension-base/src/background/RequestExtrinsicSign.ts#L18-L22 const payload = registry.createType('ExtrinsicPayload', payloadJSON, { version: payloadJSON.version }); const result = payload.sign(pair); @@ -185,6 +187,7 @@ export default class WalletState { const pair = this.#keyring.getSigningPair(payloadJSON.address); pair.unlock(password); + // https://github.com/polkadot-js/extension/blob/master/packages/extension-base/src/background/RequestBytesSign.ts#L20-L27 const signature = u8aToHex(pair.sign(u8aWrapBytes(payloadJSON.data))); resolve({ From dbd7019c194b41762bc2d2bd994ce0d6d650c445 Mon Sep 17 00:00:00 2001 From: "Thang X. Vu" Date: Thu, 6 Apr 2023 17:04:04 +0700 Subject: [PATCH 7/8] test: add specs for signing raw messages (#8) --- .../__tests__/RequestSigning.spec.tsx | 304 ++++++++++++++++++ .../RequestTransactionApproval.spec.tsx | 179 ----------- 2 files changed, 304 insertions(+), 179 deletions(-) create mode 100644 packages/ui/src/components/pages/Request/RequestSigning/__tests__/RequestSigning.spec.tsx delete mode 100644 packages/ui/src/components/pages/Request/RequestSigning/__tests__/RequestTransactionApproval.spec.tsx diff --git a/packages/ui/src/components/pages/Request/RequestSigning/__tests__/RequestSigning.spec.tsx b/packages/ui/src/components/pages/Request/RequestSigning/__tests__/RequestSigning.spec.tsx new file mode 100644 index 00000000..faf8d8ac --- /dev/null +++ b/packages/ui/src/components/pages/Request/RequestSigning/__tests__/RequestSigning.spec.tsx @@ -0,0 +1,304 @@ +import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types'; +import { defaultNetwork, newWalletErrorResponse, newWalletRequest } from '@coong/base'; +import { WalletRequestMessage } from '@coong/base/types'; +import Keyring from '@coong/keyring'; +import { AccountInfo } from '@coong/keyring/types'; +import { SpyInstance } from '@vitest/spy'; +import { + initializeKeyring, + newUser, + PASSWORD, + render, + RouterWrapper, + screen, + setupAuthorizedApps, + UserEvent, + waitFor, +} from '__tests__/testUtils'; +import { Mock } from 'vitest'; +import Request from '../../index'; +import RequestSignRawMessage from '../RequestSignRawMessage'; + +const preloadedState = { app: { seedReady: true, addressPrefix: defaultNetwork.prefix } }; +let windowClose: SpyInstance, postMessage: Mock, user: UserEvent, account01: AccountInfo, keyring: Keyring; + +beforeEach(async () => { + windowClose = vi.spyOn(window, 'close').mockImplementation(() => vi.fn()); + + postMessage = vi.fn(); + window.opener = { postMessage }; + + keyring = await initializeKeyring(); + account01 = await keyring.createNewAccount('Account 01', PASSWORD); + + user = newUser(); +}); + +describe('RequestTransactionApproval', () => { + const newPayload = (address: string) => { + return { + specVersion: '0x00002490', + transactionVersion: '0x00000013', + address, + blockHash: '0x740c0ff582a5f5ed089a83afe396be64db42486397ee23611811e123a70bd63f', + blockNumber: '0x00dd836d', + era: '0xd502', + genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3', + method: '0x050000004769bbe59968882c1597ec1151621f0193547285125f1c1337371c013ff61f02890700', + nonce: '0x00000000', + signedExtensions: [ + 'CheckNonZeroSender', + 'CheckSpecVersion', + 'CheckTxVersion', + 'CheckGenesis', + 'CheckMortality', + 'CheckNonce', + 'CheckWeight', + 'ChargeTransactionPayment', + 'PrevalidateAttests', + ], + tip: '0x00000000000000000000000000000000', + version: 4, + }; + }; + + let requestUrl: string, requestSignExtrinsic: WalletRequestMessage; + + beforeEach(async () => { + requestSignExtrinsic = newWalletRequest({ name: 'tab/signExtrinsic', body: newPayload(account01.address) }); + const queryParam = new URLSearchParams({ + message: JSON.stringify(requestSignExtrinsic), + }).toString(); + requestUrl = `/request?${queryParam}`; + }); + + describe('app is not authorized', () => { + it('should reject request & close window', async () => { + render( + + + , + { preloadedState }, + ); + + await waitFor(() => { + const expectedResponse = newWalletErrorResponse( + `The app at ${requestSignExtrinsic.origin} has not been authorized yet!`, + requestSignExtrinsic.id, + ); + expect(postMessage).toHaveBeenNthCalledWith(2, expectedResponse, requestSignExtrinsic.origin); + }); + }); + }); + + describe('app is authorized', () => { + beforeEach(() => { + setupAuthorizedApps([account01.address], window.location.origin); + }); + + it('should display the request correctly', async () => { + render( + + + , + { preloadedState }, + ); + + expect(await screen.findByText(/Transaction Approval Request/)).toBeInTheDocument(); + expect(await screen.findByText(/You are approving a transaction with account/)).toBeInTheDocument(); + expect(await screen.findByText(/Account 01/)).toBeInTheDocument(); + expect(await screen.findByTestId('row-from')).toHaveTextContent(`from: ${window.location.origin}`); + expect(await screen.findByTestId('row-method-data')).toHaveTextContent( + `method data: ${(requestSignExtrinsic.request.body as SignerPayloadJSON).method}`, + ); + + expect(await screen.findByLabelText('Wallet password')).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: /Approve Transaction/ })).toBeDisabled(); + expect(await screen.findByRole('button', { name: /Cancel/ })).toBeEnabled(); + }); + + it('should show error message for incorrect password', async () => { + render( + + + , + { preloadedState }, + ); + + const passwordField = await screen.findByLabelText('Wallet password'); + await user.type(passwordField, 'incorrect-password'); + + const approvalButton = await screen.findByRole('button', { name: /Approve Transaction/ }); + expect(approvalButton).toBeEnabled(); + await user.click(approvalButton); + + expect(await screen.findByText('Password incorrect')).toBeInTheDocument(); + }); + + it('should post back message and close window for correct password', async () => { + render( + + + , + { preloadedState }, + ); + + const passwordField = await screen.findByLabelText('Wallet password'); + await user.type(passwordField, PASSWORD); + + const approvalButton = await screen.findByRole('button', { name: /Approve Transaction/ }); + expect(approvalButton).toBeEnabled(); + await user.click(approvalButton); + + await waitFor(() => { + expect(postMessage).toHaveBeenCalledTimes(2); // 1: initialized signal, 2: signature message + }); + expect(windowClose).toBeCalled(); + }); + + it('should post back message and close window if cancelling', async () => { + render( + + + , + { preloadedState }, + ); + + const cancelButton = await screen.findByRole('button', { name: /Cancel/ }); + expect(cancelButton).toBeEnabled(); + await user.click(cancelButton); + + await waitFor(() => { + const expectedResponse = newWalletErrorResponse('Cancelled', requestSignExtrinsic.id); + expect(postMessage).toHaveBeenNthCalledWith(2, expectedResponse, requestSignExtrinsic.origin); + }); + expect(windowClose).toBeCalled(); + }); + }); +}); + +describe('RequestSignRawMessage', () => { + const newPayloadRaw = (address: string) => { + return { + address, + type: 'bytes', + data: 'This is a dummy message to sign', + } as SignerPayloadRaw; + }; + + let requestUrl: string, requestSignRawMessage: WalletRequestMessage; + + beforeEach(async () => { + requestSignRawMessage = newWalletRequest({ name: 'tab/signRaw', body: newPayloadRaw(account01.address) }); + const queryParam = new URLSearchParams({ + message: JSON.stringify(requestSignRawMessage), + }).toString(); + requestUrl = `/request?${queryParam}`; + + user = newUser(); + }); + + describe('app is not authorized', () => { + it('should reject request & close window', async () => { + render( + + + , + { preloadedState }, + ); + + await waitFor(() => { + const expectedResponse = newWalletErrorResponse( + `The app at ${requestSignRawMessage.origin} has not been authorized yet!`, + requestSignRawMessage.id, + ); + expect(postMessage).toHaveBeenNthCalledWith(2, expectedResponse, requestSignRawMessage.origin); + }); + }); + }); + + describe('app is authorized', () => { + beforeEach(() => { + setupAuthorizedApps([account01.address], window.location.origin); + }); + + it('should render the request correctly', async () => { + render( + + + , + { preloadedState }, + ); + + expect(await screen.findByText(/Sign Message Request/)).toBeInTheDocument(); + expect(await screen.findByText(/You are signing a message with account/)).toBeInTheDocument(); + expect(await screen.findByText(/Account 01/)).toBeInTheDocument(); + expect(await screen.findByTestId('row-from')).toHaveTextContent(`from: ${window.location.origin}`); + expect(await screen.findByTestId('row-bytes')).toHaveTextContent( + `bytes: ${(requestSignRawMessage.request.body as SignerPayloadRaw).data}`, + ); + + expect(await screen.findByLabelText('Wallet password')).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: /Sign Message/ })).toBeDisabled(); + expect(await screen.findByRole('button', { name: /Cancel/ })).toBeEnabled(); + }); + + it('should show error message for incorrect password', async () => { + render( + + + , + { preloadedState }, + ); + + const passwordField = await screen.findByLabelText('Wallet password'); + await user.type(passwordField, 'incorrect-password'); + + const approvalButton = await screen.findByRole('button', { name: /Sign Message/ }); + expect(approvalButton).toBeEnabled(); + await user.click(approvalButton); + + expect(await screen.findByText('Password incorrect')).toBeInTheDocument(); + }); + + it('should post back message and close window for correct password', async () => { + render( + + + , + { preloadedState }, + ); + + const passwordField = await screen.findByLabelText('Wallet password'); + await user.type(passwordField, PASSWORD); + + const approvalButton = await screen.findByRole('button', { name: /Sign Message/ }); + expect(approvalButton).toBeEnabled(); + await user.click(approvalButton); + + await waitFor(() => { + expect(postMessage).toHaveBeenCalledTimes(2); // 1: initialized signal, 2: signature message + }); + expect(windowClose).toBeCalled(); + }); + + it('should post back message and close window if cancelling', async () => { + render( + + + , + { preloadedState }, + ); + + const cancelButton = await screen.findByRole('button', { name: /Cancel/ }); + expect(cancelButton).toBeEnabled(); + await user.click(cancelButton); + + await waitFor(() => { + const expectedResponse = newWalletErrorResponse('Cancelled', requestSignRawMessage.id); + expect(postMessage).toHaveBeenNthCalledWith(2, expectedResponse, requestSignRawMessage.origin); + }); + expect(windowClose).toBeCalled(); + }); + }); +}); diff --git a/packages/ui/src/components/pages/Request/RequestSigning/__tests__/RequestTransactionApproval.spec.tsx b/packages/ui/src/components/pages/Request/RequestSigning/__tests__/RequestTransactionApproval.spec.tsx deleted file mode 100644 index e758c5ab..00000000 --- a/packages/ui/src/components/pages/Request/RequestSigning/__tests__/RequestTransactionApproval.spec.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { defaultNetwork, newWalletErrorResponse, newWalletRequest } from '@coong/base'; -import { WalletRequestMessage } from '@coong/base/types'; -import Keyring from '@coong/keyring'; -import { AccountInfo } from '@coong/keyring/types'; -import { SpyInstance } from '@vitest/spy'; -import { - initializeKeyring, - newUser, - PASSWORD, - render, - RouterWrapper, - screen, - setupAuthorizedApps, - UserEvent, - waitFor, -} from '__tests__/testUtils'; -import { Mock } from 'vitest'; -import Request from '../../index'; - -const newPayload = (address: string) => { - return { - specVersion: '0x00002490', - transactionVersion: '0x00000013', - address, - blockHash: '0x740c0ff582a5f5ed089a83afe396be64db42486397ee23611811e123a70bd63f', - blockNumber: '0x00dd836d', - era: '0xd502', - genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3', - method: '0x050000004769bbe59968882c1597ec1151621f0193547285125f1c1337371c013ff61f02890700', - nonce: '0x00000000', - signedExtensions: [ - 'CheckNonZeroSender', - 'CheckSpecVersion', - 'CheckTxVersion', - 'CheckGenesis', - 'CheckMortality', - 'CheckNonce', - 'CheckWeight', - 'ChargeTransactionPayment', - 'PrevalidateAttests', - ], - tip: '0x00000000000000000000000000000000', - version: 4, - }; -}; - -describe('RequestTransactionApproval', () => { - const preloadedState = { app: { seedReady: true, addressPrefix: defaultNetwork.prefix } }; - let windowClose: SpyInstance, - postMessage: Mock, - requestUrl: string, - user: UserEvent, - requestAccessMessage: WalletRequestMessage, - account01: AccountInfo, - keyring: Keyring; - - beforeEach(async () => { - windowClose = vi.spyOn(window, 'close').mockImplementation(() => vi.fn()); - - postMessage = vi.fn(); - window.opener = { postMessage }; - - keyring = await initializeKeyring(); - account01 = await keyring.createNewAccount('Account 01', PASSWORD); - - requestAccessMessage = newWalletRequest({ name: 'tab/signExtrinsic', body: newPayload(account01.address) }); - const queryParam = new URLSearchParams({ - message: JSON.stringify(requestAccessMessage), - }).toString(); - requestUrl = `/request?${queryParam}`; - - user = newUser(); - }); - - describe('app is not authorized', () => { - it('should reject request & close window', async () => { - render( - - - , - { preloadedState }, - ); - - await waitFor(() => { - const expectedResponse = newWalletErrorResponse( - `The app at ${requestAccessMessage.origin} has not been authorized yet!`, - requestAccessMessage.id, - ); - expect(postMessage).toHaveBeenNthCalledWith(2, expectedResponse, requestAccessMessage.origin); - }); - }); - }); - - describe('app is authorized', () => { - beforeEach(() => { - setupAuthorizedApps([account01.address], window.location.origin); - }); - - it('should display the request correctly', async () => { - render( - - - , - { preloadedState }, - ); - - expect(await screen.findByText(/Transaction Approval Request/)).toBeInTheDocument(); - expect(await screen.findByText(/You are approving a transaction with account/)).toBeInTheDocument(); - expect(await screen.findByText(/Account 01/)).toBeInTheDocument(); - expect(await screen.findByTestId('row-from')).toHaveTextContent(`from: ${window.location.origin}`); - expect(await screen.findByTestId('row-method-data')).toHaveTextContent( - // @ts-ignore - `method data: ${requestAccessMessage.request.body.method}`, - ); - - expect(await screen.findByLabelText('Wallet password')).toBeInTheDocument(); - expect(await screen.findByRole('button', { name: /Approve Transaction/ })).toBeDisabled(); - expect(await screen.findByRole('button', { name: /Cancel/ })).toBeEnabled(); - }); - - it('should show error message for incorrect password', async () => { - render( - - - , - { preloadedState }, - ); - - const passwordField = await screen.findByLabelText('Wallet password'); - await user.type(passwordField, 'incorrect-password'); - - const approvalButton = await screen.findByRole('button', { name: /Approve Transaction/ }); - expect(approvalButton).toBeEnabled(); - await user.click(approvalButton); - - expect(await screen.findByText('Password incorrect')).toBeInTheDocument(); - }); - - it('should post back message and close window for correct password', async () => { - render( - - - , - { preloadedState }, - ); - - const passwordField = await screen.findByLabelText('Wallet password'); - await user.type(passwordField, PASSWORD); - - const approvalButton = await screen.findByRole('button', { name: /Approve Transaction/ }); - expect(approvalButton).toBeEnabled(); - await user.click(approvalButton); - - await waitFor(() => { - expect(postMessage).toHaveBeenCalledTimes(2); // 1: initialized signal, 2: signature message - }); - expect(windowClose).toBeCalled(); - }); - - it('should post back message and close window if cancelling', async () => { - render( - - - , - { preloadedState }, - ); - - const cancelButton = await screen.findByRole('button', { name: /Cancel/ }); - expect(cancelButton).toBeEnabled(); - await user.click(cancelButton); - - await waitFor(() => { - const expectedResponse = newWalletErrorResponse('Cancelled', requestAccessMessage.id); - expect(postMessage).toHaveBeenNthCalledWith(2, expectedResponse, requestAccessMessage.origin); - }); - expect(windowClose).toBeCalled(); - }); - }); -}); From 09560c1d03dcaf5309e6e87b9c45bb8cc8684eb9 Mon Sep 17 00:00:00 2001 From: "Thang X. Vu" Date: Sat, 8 Apr 2023 11:33:38 +0700 Subject: [PATCH 8/8] style: move comment --- .../Request/RequestSigning/RequestTransactionApproval.tsx | 5 ----- .../src/components/pages/Request/RequestSigning/SignArea.tsx | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/components/pages/Request/RequestSigning/RequestTransactionApproval.tsx b/packages/ui/src/components/pages/Request/RequestSigning/RequestTransactionApproval.tsx index 7754ae9b..757c00b9 100644 --- a/packages/ui/src/components/pages/Request/RequestSigning/RequestTransactionApproval.tsx +++ b/packages/ui/src/components/pages/Request/RequestSigning/RequestTransactionApproval.tsx @@ -15,11 +15,6 @@ const RequestTransactionApproval: FC = ({ className, message }) => const detailRows = useTransactionDetails(message); const approveTransaction = async (password: string) => { - // signing & accounts decryption are synchronous operations - // and might take some time to do - // so we delay it a short amount of time to make sure the UI could be updated (disable button, ...) - // before the signing process begin - // TODO: Moving CPU-intensive operations to worker await walletState.approveSignExtrinsic(password); }; diff --git a/packages/ui/src/components/pages/Request/RequestSigning/SignArea.tsx b/packages/ui/src/components/pages/Request/RequestSigning/SignArea.tsx index 63d24e95..d345df4c 100644 --- a/packages/ui/src/components/pages/Request/RequestSigning/SignArea.tsx +++ b/packages/ui/src/components/pages/Request/RequestSigning/SignArea.tsx @@ -22,6 +22,11 @@ const SignArea: FC = ({ onSign, onCancel, cancelButtonLabel = 'Ca e.preventDefault(); toggleLoading(true); + // signing & accounts decryption are synchronous operations + // and might take some time to do + // so we delay it a short amount of time to make sure the UI could be updated (disable button, ...) + // before the signing process begin + // TODO: Moving CPU-intensive operations to worker setTimeout(async () => { try { await onSign(password);