From 0e789cfc91ab721410a3b79832784cadbc392ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=BB=C3=B3=C5=82kiewski?= Date: Fri, 31 Jan 2020 16:13:56 +0100 Subject: [PATCH] Expose signed transactions (#2203) * sign offline WIP * signing transactions from extrinsics screen * signing tx from accounts screen * added translations * removed unnecessary value * removed console logs * removed unused import * used new signAsync method * fixed types * removed unused imports * added missing translations * code review changes * fixed signing with local account * added sign toggle * improvements for QR --- .../react-components/src/ButtonCancel.tsx | 5 +- packages/react-components/src/Modal.tsx | 8 +- packages/react-components/src/TxButton.tsx | 2 +- packages/react-signer/src/Modal.tsx | 429 ++++++++++++------ packages/react-signer/src/index.tsx | 5 + 5 files changed, 304 insertions(+), 145 deletions(-) diff --git a/packages/react-components/src/ButtonCancel.tsx b/packages/react-components/src/ButtonCancel.tsx index 75be25d68b1..228c0fe3b0e 100644 --- a/packages/react-components/src/ButtonCancel.tsx +++ b/packages/react-components/src/ButtonCancel.tsx @@ -10,11 +10,12 @@ import { useTranslation } from './translate'; interface Props { className?: string; isDisabled?: boolean; + label?: string; onClick: () => void; tabIndex?: number; } -export default function ButtonCancel ({ className, isDisabled, onClick, tabIndex }: Props): React.ReactElement { +export default function ButtonCancel ({ className, isDisabled, label, onClick, tabIndex }: Props): React.ReactElement { const { t } = useTranslation(); return ( @@ -23,7 +24,7 @@ export default function ButtonCancel ({ className, isDisabled, onClick, tabIndex icon='cancel' isDisabled={isDisabled} isNegative - label={t('Cancel')} + label={label || t('Cancel')} onClick={onClick} tabIndex={tabIndex} /> diff --git a/packages/react-components/src/Modal.tsx b/packages/react-components/src/Modal.tsx index 93e2d427a44..d9206a1c7fa 100644 --- a/packages/react-components/src/Modal.tsx +++ b/packages/react-components/src/Modal.tsx @@ -19,7 +19,9 @@ interface ModalProps extends BareProps { } interface ActionsProps extends BareProps { + cancelLabel?: string; children: React.ReactNode; + withOr?: boolean; onCancel: () => void; } @@ -42,12 +44,12 @@ function Modal (props: ModalProps): React.ReactElement { ); } -function Actions ({ className, children, onCancel }: ActionsProps): React.ReactElement { +function Actions ({ cancelLabel, className, children, withOr = true, onCancel }: ActionsProps): React.ReactElement { return ( - - + + {withOr && } {children} diff --git a/packages/react-components/src/TxButton.tsx b/packages/react-components/src/TxButton.tsx index 2460b411041..fd8fca35663 100644 --- a/packages/react-components/src/TxButton.tsx +++ b/packages/react-components/src/TxButton.tsx @@ -62,7 +62,7 @@ class TxButtonInner extends React.PureComponent { } protected send = (): void => { - const { accountId, api, extrinsic: propsExtrinsic, isUnsigned, onClick, onFailed, onStart, onSuccess, onUpdate, params = [], queueExtrinsic, tx = '', withSpinner = false } = this.props; + const { accountId, api, extrinsic: propsExtrinsic, isUnsigned, onClick, onFailed, onStart, onSuccess, onUpdate, params = [], queueExtrinsic, tx = '', withSpinner = true } = this.props; let extrinsic: any; if (propsExtrinsic) { diff --git a/packages/react-signer/src/Modal.tsx b/packages/react-signer/src/Modal.tsx index cf41c949028..1266990b0da 100644 --- a/packages/react-signer/src/Modal.tsx +++ b/packages/react-signer/src/Modal.tsx @@ -2,14 +2,19 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { SignerOptions, SignerResult } from '@polkadot/api/types'; +import { SignerOptions, SignerResult, Signer as ApiSigner } from '@polkadot/api/types'; import { SubmittableExtrinsic } from '@polkadot/api/promise/types'; import { ApiProps } from '@polkadot/react-api/types'; import { I18nProps, BareProps } from '@polkadot/react-components/types'; import { RpcMethod } from '@polkadot/jsonrpc/types'; import { KeyringPair } from '@polkadot/keyring/types'; import { SubjectInfo } from '@polkadot/ui-keyring/observable/types'; -import { QueueTx, QueueTxMessageSetStatus, QueueTxResult, QueueTxStatus } from '@polkadot/react-components/Status/types'; +import { + QueueTx, + QueueTxMessageSetStatus, + QueueTxResult, + QueueTxStatus +} from '@polkadot/react-components/Status/types'; import { SignerPayloadJSON } from '@polkadot/types/types'; import BN from 'bn.js'; @@ -17,7 +22,7 @@ import React from 'react'; import { SubmittableResult } from '@polkadot/api'; import { web3FromSource } from '@polkadot/extension-dapp'; import { createType } from '@polkadot/types'; -import { Button, InputBalance, Modal, Toggle, ErrorBoundary } from '@polkadot/react-components'; +import { Button, InputBalance, Modal, Toggle, Output, ErrorBoundary, InputNumber } from '@polkadot/react-components'; import { registry } from '@polkadot/react-api'; import { withApi, withMulti, withObservable } from '@polkadot/react-api/hoc'; import keyring from '@polkadot/ui-keyring'; @@ -40,25 +45,32 @@ interface Props extends I18nProps, ApiProps, BaseProps { } interface State { + accountNonce?: string; + blocks: string; currentItem?: QueueTx; isQrScanning: boolean; isQrVisible: boolean; isRenderError: boolean; isSendable: boolean; + isSubmit: boolean; isV2?: boolean; + nonce?: string; password: string; qrAddress: string; qrPayload: Uint8Array; qrResolve?: (result: SignerResult) => void; qrReject?: (error: Error) => void; showTip: boolean; + signedTx?: string; tip?: BN; unlockError?: string | null; } let qrId = 0; -function extractExternal (accountId?: string | null): { isExternal: boolean; isHardware: boolean; hardwareType?: string } { +function extractExternal ( + accountId?: string | null +): { isExternal: boolean; isHardware: boolean; hardwareType?: string } { if (!accountId) { return { isExternal: false, isHardware: false }; } @@ -83,7 +95,11 @@ function extractExternal (accountId?: string | null): { isExternal: boolean; isH } // eslint-disable-next-line @typescript-eslint/require-await -async function makeExtrinsicSignature (payload: SignerPayloadJSON, { id, signerCb }: QueueTx, pair: KeyringPair): Promise { +async function makeExtrinsicSignature ( + payload: SignerPayloadJSON, + { id, signerCb }: QueueTx, + pair: KeyringPair +): Promise { console.log('makeExtrinsicSignature: payload ::', JSON.stringify(payload)); const result = createType(registry, 'ExtrinsicPayload', payload, { version: payload.version }).sign(pair); @@ -93,20 +109,30 @@ async function makeExtrinsicSignature (payload: SignerPayloadJSON, { id, signerC } } +const initialState: State = { + accountNonce: undefined, + blocks: '50', + isQrScanning: false, + isQrVisible: false, + isRenderError: false, + isSendable: false, + isSubmit: true, + nonce: undefined, + password: '', + qrAddress: '', + qrPayload: new Uint8Array(), + showTip: false, + signedTx: '', + unlockError: null +}; + class Signer extends React.PureComponent { - public state: State = { - isQrScanning: false, - isQrVisible: false, - isRenderError: false, - isSendable: false, - password: '', - qrAddress: '', - qrPayload: new Uint8Array(), - showTip: false, - unlockError: null - }; + public state: State = initialState; - public static getDerivedStateFromProps ({ allAccounts, api, queue }: Props, { currentItem, password, unlockError }: State): Partial { + public static getDerivedStateFromProps ( + { allAccounts, api, queue }: Props, + { currentItem, password, unlockError }: State + ): Partial { let isV2: boolean; try { isV2 = !!api.tx.session.setKeys; @@ -118,12 +144,9 @@ class Signer extends React.PureComponent { const isSame = !!nextItem && !!currentItem && - ( - (!nextItem.accountId && !currentItem.accountId) || - ( - (nextItem.accountId && nextItem.accountId.toString()) === (currentItem.accountId && currentItem.accountId.toString()) - ) - ); + ((!nextItem.accountId && !currentItem.accountId) || + (nextItem.accountId && nextItem.accountId.toString()) === + (currentItem.accountId && currentItem.accountId.toString())); let isSendable = !!nextItem && !!nextItem.isUnsigned; @@ -147,11 +170,15 @@ class Signer extends React.PureComponent { } public async componentDidUpdate (): Promise { - const { currentItem } = this.state; + const { accountNonce, currentItem, isSubmit } = this.state; if (currentItem && currentItem.status === 'queued' && !(currentItem.extrinsic || currentItem.payload)) { return this.sendRpc(currentItem); } + + if (!isSubmit && currentItem?.accountId && accountNonce == null) { + this.updateNonce(); + } } public render (): React.ReactNode { @@ -164,9 +191,7 @@ class Signer extends React.PureComponent { return ( - - {this.renderContent()} - + {this.renderContent()} {this.renderButtons()} ); @@ -174,7 +199,7 @@ class Signer extends React.PureComponent { private renderButtons (): React.ReactNode { const { t } = this.props; - const { currentItem, isQrScanning, isQrVisible, isRenderError, isSendable } = this.state; + const { currentItem, isQrScanning, isQrVisible, isRenderError, isSendable, isSubmit, signedTx } = this.state; if (!currentItem) { return null; @@ -183,25 +208,16 @@ class Signer extends React.PureComponent { const { isExternal, isHardware, hardwareType } = extractExternal(currentItem.accountId); return ( - - {!isRenderError && (!isQrVisible || !isQrScanning) && ( + + {!isRenderError && (!isQrVisible || !isQrScanning) && !signedTx && ( <> + {!currentItem.isUnsigned && this.renderSignToggle()}