diff --git a/packages/react-signer/src/Address.tsx b/packages/react-signer/src/Address.tsx index de23d508d7..1866765b9c 100644 --- a/packages/react-signer/src/Address.tsx +++ b/packages/react-signer/src/Address.tsx @@ -7,7 +7,7 @@ import { AccountId, BalanceOf, Call, Multisig, ProxyDefinition, ProxyType } from import { ITuple } from '@polkadot/types/types'; import { AddressFlags, AddressProxy } from './types'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { ApiPromise } from '@polkadot/api'; import { InputAddress, Modal, Toggle } from '@polkadot/react-components'; import { useAccounts, useApi, useIsMountedRef } from '@polkadot/react-hooks'; @@ -34,6 +34,11 @@ interface MultiState { whoFilter: string[]; } +interface PasswordState { + isUnlockCached: boolean; + signPassword: string; +} + interface ProxyState { address: string; isProxied: boolean; @@ -146,7 +151,7 @@ function Address ({ currentItem, onChange, onEnter, passwordError, requestAddres const [isProxyActive, setIsProxyActive] = useState(true); const [multiInfo, setMultInfo] = useState(null); const [proxyInfo, setProxyInfo] = useState(null); - const [signPassword, setSignPassword] = useState(''); + const [{ isUnlockCached, signPassword }, setSignPassword] = useState({ isUnlockCached: false, signPassword: '' }); const [signAddress, flags] = useMemo( (): [string, AddressFlags] => { @@ -159,6 +164,11 @@ function Address ({ currentItem, onChange, onEnter, passwordError, requestAddres [multiAddress, proxyAddress, isProxyActive, multiInfo, proxyInfo, requestAddress] ); + const _updatePassword = useCallback( + (signPassword: string, isUnlockCached: boolean) => setSignPassword({ isUnlockCached, signPassword }), + [] + ); + useEffect((): void => { !proxyInfo && setProxyAddress(null); }, [proxyInfo]); @@ -190,13 +200,14 @@ function Address ({ currentItem, onChange, onEnter, passwordError, requestAddres useEffect((): void => { onChange({ - isMultiCall: isMultiCall, + isMultiCall, + isUnlockCached, multiRoot: multiInfo ? multiInfo.address : null, proxyRoot: (proxyInfo && isProxyActive) ? proxyInfo.address : null, signAddress, signPassword }); - }, [isProxyActive, isMultiCall, multiAddress, multiInfo, onChange, proxyAddress, proxyInfo, signAddress, signPassword]); + }, [isProxyActive, isMultiCall, isUnlockCached, multiAddress, multiInfo, onChange, proxyAddress, proxyInfo, signAddress, signPassword]); return ( <> @@ -251,7 +262,7 @@ function Address ({ currentItem, onChange, onEnter, passwordError, requestAddres )} diff --git a/packages/react-signer/src/Password.tsx b/packages/react-signer/src/Password.tsx index 484bdcc17e..e13cdf698a 100644 --- a/packages/react-signer/src/Password.tsx +++ b/packages/react-signer/src/Password.tsx @@ -5,16 +5,17 @@ import { KeyringPair } from '@polkadot/keyring/types'; import React, { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; -import { Modal, Password } from '@polkadot/react-components'; +import { Modal, Password, Toggle } from '@polkadot/react-components'; import keyring from '@polkadot/ui-keyring'; import { useTranslation } from './translate'; +import { UNLOCK_MINS } from './util'; interface Props { address: string; className?: string; error?: string; - onChange: (password: string) => void; + onChange: (password: string, isUnlockCached: boolean) => void; onEnter?: () => void; password: string; tabIndex?: number; @@ -31,6 +32,7 @@ function getPair (address: string): KeyringPair | null { function Unlock ({ address, className, error, onChange, onEnter, tabIndex }: Props): React.ReactElement | null { const { t } = useTranslation(); const [password, setPassword] = useState(''); + const [isUnlockCached, setIsUnlockCached] = useState(false); const pair = useMemo( () => getPair(address), @@ -38,8 +40,8 @@ function Unlock ({ address, className, error, onChange, onEnter, tabIndex }: Pro ); useEffect((): void => { - onChange(password); - }, [onChange, password]); + onChange(password, isUnlockCached); + }, [onChange, isUnlockCached, password]); if (!pair || !pair.isLocked || pair.meta.isInjected) { return null; @@ -51,13 +53,19 @@ function Unlock ({ address, className, error, onChange, onEnter, tabIndex }: Pro {t('wrong password supplied')}} + label={t('unlock account with password')} onChange={setPassword} onEnter={onEnter} tabIndex={tabIndex} value={password} - /> + > + ('unlock for {{expiry}} min', { replace: { expiry: UNLOCK_MINS } })} + onChange={setIsUnlockCached} + value={isUnlockCached} + /> +

{t('Unlock the sending account to allow signing of this transaction.')}

@@ -68,7 +76,11 @@ function Unlock ({ address, className, error, onChange, onEnter, tabIndex }: Pro export default React.memo(styled(Unlock)` .errorLabel { - margin-right: 2rem; + margin-right: 1rem; color: #9f3a38 !important; } + + .ui--Toggle { + bottom: 1.1rem; + } `); diff --git a/packages/react-signer/src/TxSigned.tsx b/packages/react-signer/src/TxSigned.tsx index 8464152599..74e7aee697 100644 --- a/packages/react-signer/src/TxSigned.tsx +++ b/packages/react-signer/src/TxSigned.tsx @@ -19,14 +19,14 @@ import { Option } from '@polkadot/types'; import keyring from '@polkadot/ui-keyring'; import { BN_ZERO, assert } from '@polkadot/util'; -import { LedgerSigner, QrSigner } from './signers'; +import { AccountSigner, LedgerSigner, QrSigner } from './signers'; import { useTranslation } from './translate'; import Address from './Address'; import Qr from './Qr'; import SignFields from './SignFields'; import Tip from './Tip'; import Transaction from './Transaction'; -import { extractExternal, handleTxResults } from './util'; +import { cacheUnlock, extractExternal, handleTxResults } from './util'; interface Props { className?: string; @@ -38,7 +38,7 @@ const NOOP = () => undefined; let qrId = 0; -function unlockAccount ({ signAddress, signPassword }: AddressProxy): string | null { +function unlockAccount ({ isUnlockCached, signAddress, signPassword }: AddressProxy): string | null { let publicKey; try { @@ -53,6 +53,7 @@ function unlockAccount ({ signAddress, signPassword }: AddressProxy): string | n try { pair.decodePkcs8(signPassword); + isUnlockCached && cacheUnlock(pair); } catch (error) { console.error(error); @@ -133,7 +134,7 @@ async function wrapTx (api: ApiPromise, currentItem: QueueTx, { isMultiCall, mul return tx; } -async function extractParams (address: string, options: Partial, setQrState: (state: QrState) => void): Promise<['qr' | 'signing', KeyringPair | string, Partial]> { +async function extractParams (address: string, options: Partial, setQrState: (state: QrState) => void): Promise<['qr' | 'signing', string, Partial]> { const pair = keyring.getPair(address); const { meta: { accountOffset, addressOffset, isExternal, isHardware, isInjected, source } } = pair; @@ -149,7 +150,7 @@ async function extractParams (address: string, options: Partial, return ['signing', address, { ...options, signer: injected.signer }]; } - return ['signing', pair, options]; + return ['signing', pair.address, { ...options, signer: new AccountSigner(pair) }]; } function TxSigned ({ className, currentItem, requestAddress }: Props): React.ReactElement | null { @@ -162,7 +163,7 @@ function TxSigned ({ className, currentItem, requestAddress }: Props): React.Rea const [isRenderError, toggleRenderError] = useToggle(); const [isSubmit, setIsSubmit] = useState(true); const [passwordError, setPasswordError] = useState(null); - const [senderInfo, setSenderInfo] = useState({ isMultiCall: false, multiRoot: null, proxyRoot: null, signAddress: requestAddress, signPassword: '' }); + const [senderInfo, setSenderInfo] = useState({ isMultiCall: false, isUnlockCached: false, multiRoot: null, proxyRoot: null, signAddress: requestAddress, signPassword: '' }); const [signedOptions, setSignedOptions] = useState>({}); const [signedTx, setSignedTx] = useState(null); const [multiCall, setMultiCall] = useState(null); diff --git a/packages/react-signer/src/signers/AccountSigner.ts b/packages/react-signer/src/signers/AccountSigner.ts index d44f4957df..64d50a1543 100644 --- a/packages/react-signer/src/signers/AccountSigner.ts +++ b/packages/react-signer/src/signers/AccountSigner.ts @@ -7,6 +7,8 @@ import { SignerPayloadJSON } from '@polkadot/types/types'; import { registry } from '@polkadot/react-api'; +import { lockAccount } from '../util'; + let id = 0; export default class AccountSigner implements Signer { @@ -20,6 +22,7 @@ export default class AccountSigner implements Signer { return new Promise((resolve): void => { const signed = registry.createType('ExtrinsicPayload', payload, { version: payload.version }).sign(this.#keyringPair); + lockAccount(this.#keyringPair); resolve({ id: ++id, ...signed }); }); } diff --git a/packages/react-signer/src/types.ts b/packages/react-signer/src/types.ts index 1bee8000b9..1283e59d59 100644 --- a/packages/react-signer/src/types.ts +++ b/packages/react-signer/src/types.ts @@ -16,6 +16,7 @@ export interface AddressFlags { export interface AddressProxy { isMultiCall: boolean; + isUnlockCached: boolean; multiRoot: string | null; proxyRoot: string | null; signAddress: string | null; diff --git a/packages/react-signer/src/util.ts b/packages/react-signer/src/util.ts index 3600175c88..fc06163670 100644 --- a/packages/react-signer/src/util.ts +++ b/packages/react-signer/src/util.ts @@ -1,6 +1,7 @@ // Copyright 2017-2020 @polkadot/react-signer authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { KeyringPair } from '@polkadot/keyring/types'; import { QueueTx, QueueTxMessageSetStatus, QueueTxStatus } from '@polkadot/react-components/Status/types'; import { AddressFlags } from './types'; @@ -9,10 +10,22 @@ import keyring from '@polkadot/ui-keyring'; const NOOP = () => undefined; -const LOCK_DELAY = 15 * 60 * 1000; +export const UNLOCK_MINS = 15; + +const LOCK_DELAY = UNLOCK_MINS * 60 * 1000; const lockCountdown: Record = {}; +export function cacheUnlock (pair: KeyringPair): void { + lockCountdown[pair.address] = Date.now() + LOCK_DELAY; +} + +export function lockAccount (pair: KeyringPair): void { + if ((Date.now() > (lockCountdown[pair.address] || 0)) && !pair.isLocked) { + pair.lock(); + } +} + export function extractExternal (accountId: string | null): AddressFlags { if (!accountId) { return { isHardware: false, isMultisig: false, isProxied: false, isQr: false, isUnlockable: false, threshold: 0, who: [] }; @@ -33,13 +46,11 @@ export function extractExternal (accountId: string | null): AddressFlags { if (isUnlockable) { const entry = lockCountdown[pair.address]; - const now = Date.now(); - if (entry && (now > entry) && !pair.isLocked) { + if (entry && (Date.now() > entry) && !pair.isLocked) { pair.lock(); + lockCountdown[pair.address] = 0; } - - lockCountdown[pair.address] = now + LOCK_DELAY; } return {