diff --git a/packages/extension-polkagate/src/components/ExtensionSignArea.tsx b/packages/extension-polkagate/src/components/ExtensionSignArea.tsx new file mode 100644 index 000000000..4ce48b753 --- /dev/null +++ b/packages/extension-polkagate/src/components/ExtensionSignArea.tsx @@ -0,0 +1,317 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-disable react/jsx-max-props-per-line */ + +import type { Header } from '@polkadot/types/interfaces'; +import type { BN } from '@polkadot/util'; +import type { HexString } from '@polkadot/util/types'; + +import { Grid, SxProps, Theme, Tooltip, useTheme } from '@mui/material'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + +import { ApiPromise } from '@polkadot/api'; +import { SubmittableExtrinsic } from '@polkadot/api/types'; +import { AccountId } from '@polkadot/types/interfaces/runtime'; +import { ISubmittableResult } from '@polkadot/types/types'; + +import { useAccount, useCanPayFee, useMetadata, useTranslation } from '../hooks'; +import { HeaderBrand } from '../partials'; +import SelectProxy from '../partials/SelectProxy'; +import Qr from '../popup/signing/Qr'; +import { CMD_MORTAL } from '../popup/signing/Request'; +import { send } from '../util/api'; +import { Proxy, ProxyItem, ProxyTypes, TxInfo } from '../util/types'; +import { noop } from '../util/utils'; +import { Identity, Password, PButton, Popup, Progress, Warning } from '.'; + +interface Props { + api: ApiPromise | undefined; + estimatedFee?: BN; + confirmDisabled?: boolean; + confirmText?: string + disabled?: boolean; + isPasswordError?: boolean; + label: string; + onChange: React.Dispatch> + proxiedAddress: string | AccountId | undefined; + genesisHash: string | undefined; + prevState?: Record; + proxyTypeFilter: ProxyTypes[]; + style?: SxProps; + proxies: ProxyItem[] | undefined + setSelectedProxy: React.Dispatch>; + selectedProxy: Proxy | undefined; + setIsPasswordError: React.Dispatch>; + onConfirmClick: () => Promise; + ptx: SubmittableExtrinsic<'promise', ISubmittableResult> | undefined; + senderAddress: string; + setTxInfo: (value: React.SetStateAction) => void; + setShowWaitScreen: (value: React.SetStateAction) => void; + setShowConfirmation: (value: React.SetStateAction) => void; + extraInfo: Record; +} + +export default function PasswordUseProxyConfirm({ api, confirmDisabled, confirmText, disabled, estimatedFee, extraInfo, genesisHash, isPasswordError, label = '', onChange, onConfirmClick, prevState, proxiedAddress, proxies, proxyTypeFilter, ptx, selectedProxy, senderAddress, setIsPasswordError, setSelectedProxy, setShowConfirmation, setShowWaitScreen, setTxInfo, style }: Props): React.ReactElement { + const { t } = useTranslation(); + const theme = useTheme(); + const canPayFee = useCanPayFee(selectedProxy?.delegate || proxiedAddress, estimatedFee); + const account = useAccount(proxiedAddress); + const chain = useMetadata(genesisHash, true); + + const [password, setPassword] = useState(); + const [showSelectProxy, setShowSelectProxy] = useState(false); + const [showQRSigner, setShowQRSigner] = useState(false); + const [lastHeader, setLastHeader] = useState
(); + const [rawNonce, setRawNonce] = useState(); + + const mustSelectProxy = useMemo(() => account?.isExternal && !account?.isQR && !selectedProxy, [account, selectedProxy]); + const mustSelectQR = useMemo(() => account?.isQR && !selectedProxy, [account, selectedProxy]); + + const proxiesToSelect = useMemo(() => proxies?.filter((proxy) => proxy.status !== 'new'), [proxies]); + + const payload = useMemo(() => { + if (!api || !ptx || !lastHeader || !rawNonce) { + return; + } + + try { + const _payload = { + address: senderAddress, + blockHash: lastHeader.hash.toHex(), + blockNumber: api.registry.createType('BlockNumber', lastHeader.number.toNumber()).toHex(), + era: api.registry.createType('ExtrinsicEra', { current: lastHeader.number.toNumber(), period: 64 }).toHex(), + genesisHash: api.genesisHash.toHex(), + method: api.createType('Call', ptx).toHex(), // TODO: DOES SUPPORT nested calls, batches , ... + nonce: api.registry.createType('Compact', rawNonce).toHex(), + signedExtensions: [ + 'CheckNonZeroSender', + 'CheckSpecVersion', + 'CheckTxVersion', + 'CheckGenesis', + 'CheckMortality', + 'CheckNonce', + 'CheckWeight', + 'ChargeTransactionPayment' + ], + specVersion: api.runtimeVersion.specVersion.toHex(), + tip: api.registry.createType('Compact', 0).toHex(), + transactionVersion: api.runtimeVersion.transactionVersion.toHex(), + version: ptx.version + }; + + return api.registry.createType('ExtrinsicPayload', _payload, { version: _payload.version }); + } catch (error) { + console.error('Something went wrong when making payload:', error); + + return undefined; + } + }, [api, senderAddress, lastHeader, rawNonce, ptx]); + + useEffect((): void => { + if (api && senderAddress) { + api.rpc.chain.getHeader().then(setLastHeader).catch(console.error); + api.query.system.account(senderAddress).then((res) => setRawNonce(res?.nonce || 0)).catch(console.error); + } + }, [api, senderAddress]); + + const onSignature = useCallback(async ({ signature }: { signature: HexString }) => { + if (!api || !payload || !signature || !ptx || !senderAddress) { + return; + } + + setShowWaitScreen(true); + + const { block, failureText, fee, success, txHash } = await send(senderAddress, api, ptx, payload, signature); + + const info = { + block: block || 0, + chain, + date: Date.now(), + failureText, + fee: fee || String(estimatedFee || 0), + from: { address: senderAddress, name }, + success, + txHash: txHash || '', + ...extraInfo + }; + + setShowWaitScreen(false); + setShowConfirmation(true); + setTxInfo({ ...info, api } as TxInfo); + }, [api, chain, estimatedFee, extraInfo, payload, ptx, senderAddress, setShowConfirmation, setShowWaitScreen, setTxInfo]); + + const _onChange = useCallback((pass: string): void => { + pass.length > 3 && pass && setPassword(pass); + pass.length > 3 && pass && setIsPasswordError && setIsPasswordError(false); + }, [setIsPasswordError]); + + const goToSelectProxy = useCallback((): void => { + setShowSelectProxy(true); + }, [setShowSelectProxy]); + + const goToQRSigner = useCallback((): void => { + setShowQRSigner(true); + }, []); + + const closeQRSigner = useCallback((): void => { + setShowQRSigner(false); + }, []); + + useEffect(() => { + onChange(password); + }, [password, onChange]); + + return ( + <> + + {mustSelectProxy + ? <> + + + {t('This is a watch-only account. To complete this transaction, you must use a proxy.')} + + + ('Use Proxy')} + /> + + : mustSelectQR + ? <> + + + {t('This is a QR-attached account. To complete this transaction, you need to use your QR-signer.')} + + + + + : canPayFee === false + ? + + {t('This account lacks the required available balance to cover the transaction fee.')} + + + : <> + + + + + {(!!proxiesToSelect?.length || prevState?.selectedProxyAddress) && + + {selectedProxy && + + } + + } + > + + {selectedProxy ? t('Update proxy') : t('Use proxy')} + + + } + + ('Confirm')} + /> + + } + + {showSelectProxy && + + } + {showQRSigner && + + + + div': { width: 'inherit' }, pt: '30px' }}> + {senderAddress && (account?.genesisHash || api?.genesisHash?.toHex()) && payload + ? + : + } + + + + } + + ); +} diff --git a/packages/extension-polkagate/src/components/index.ts b/packages/extension-polkagate/src/components/index.ts index 238fa8a5a..c7bc06097 100644 --- a/packages/extension-polkagate/src/components/index.ts +++ b/packages/extension-polkagate/src/components/index.ts @@ -92,5 +92,6 @@ export { default as DisplayLogo } from './DisplayLogo'; export { default as FullScreenIcon } from './FullScreenIcon'; export { default as OptionalCopyButton } from './OptionalCopyButton'; export { default as Waiting } from './Waiting'; +export { default as ExtensionSignArea } from './ExtensionSignArea'; export * from './contexts'; diff --git a/packages/extension-polkagate/src/popup/manageProxies/Review.tsx b/packages/extension-polkagate/src/popup/manageProxies/Review.tsx index 8515a59f1..5ce374817 100644 --- a/packages/extension-polkagate/src/popup/manageProxies/Review.tsx +++ b/packages/extension-polkagate/src/popup/manageProxies/Review.tsx @@ -14,7 +14,7 @@ import { Chain } from '@polkadot/extension-chains/types'; import keyring from '@polkadot/ui-keyring'; import { BN, BN_ONE } from '@polkadot/util'; -import { ActionContext, CanPayErrorAlert, PasswordUseProxyConfirm, ProxyTable, ShowBalance, WrongPasswordAlert } from '../../components'; +import { ActionContext, CanPayErrorAlert, ExtensionSignArea, PasswordUseProxyConfirm, ProxyTable, ShowBalance, WrongPasswordAlert } from '../../components'; import { useAccount, useAccountDisplay, useCanPayFeeAndDeposit } from '../../hooks'; import useTranslation from '../../hooks/useTranslation'; import { SubTitle, WaitScreen } from '../../partials'; @@ -93,6 +93,15 @@ export default function Review({ address, api, chain, depositToPay, depositValue void tx.paymentInfo(formatted).then((i) => setEstimatedFee(i?.partialFee)); }, [api, formatted, tx]); + const ptx = useMemo(() => { + return selectedProxy ? api.tx.proxy.proxy(formatted, selectedProxy.proxyType, tx) : tx; + }, [api.tx.proxy, formatted, selectedProxy, tx]); + + const extraInfo = { + action: 'Manage Proxy', + subAction: 'Add/Remove Proxy' + }; + const onNext = useCallback(async (): Promise => { try { const from = selectedProxy?.delegate ?? formatted; @@ -101,9 +110,7 @@ export default function Review({ address, api, chain, depositToPay, depositValue signer.unlock(password); setShowWaitScreen(true); - const decidedTx = selectedProxy ? api.tx.proxy.proxy(formatted, selectedProxy.proxyType, tx) : tx; - - const { block, failureText, fee, success, txHash } = await signAndSend(api, decidedTx, signer, selectedProxy?.delegate ?? formatted); + const { block, failureText, fee, success, txHash } = await signAndSend(api, ptx, signer, selectedProxy?.delegate ?? formatted); const info = { action: 'Manage Proxy', @@ -127,7 +134,7 @@ export default function Review({ address, api, chain, depositToPay, depositValue console.log('error:', e); setIsPasswordError(true); } - }, [api, chain, estimatedFee, formatted, name, password, selectedProxy, selectedProxyAddress, selectedProxyName, tx]); + }, [api, chain, estimatedFee, formatted, name, password, ptx, selectedProxy?.delegate, selectedProxyAddress, selectedProxyName]); useEffect(() => { const addingLength = proxies.filter((item) => item.status === 'new').length; @@ -184,7 +191,7 @@ export default function Review({ address, api, chain, depositToPay, depositValue - + {t('Fee:')} @@ -198,10 +205,10 @@ export default function Review({ address, api, chain, depositToPay, depositValue - ('Password for {{name}}', { replace: { name: selectedProxyName || name || '' } })} @@ -210,9 +217,14 @@ export default function Review({ address, api, chain, depositToPay, depositValue proxiedAddress={address} proxies={proxies} proxyTypeFilter={['Any', 'NonTransfer']} + ptx={ptx} selectedProxy={selectedProxy} + senderAddress={formatted} setIsPasswordError={setIsPasswordError} setSelectedProxy={setSelectedProxy} + setShowConfirmation={setShowConfirmation} + setShowWaitScreen={setShowWaitScreen} + setTxInfo={setTxInfo} style={{ bottom: '80px', left: '4%',