diff --git a/packages/app-contracts/src/Code.tsx b/packages/app-contracts/src/Code.tsx index 94d73edfcc..92fe6f7fe6 100644 --- a/packages/app-contracts/src/Code.tsx +++ b/packages/app-contracts/src/Code.tsx @@ -9,10 +9,11 @@ import { ComponentProps } from './types'; import BN from 'bn.js'; import React from 'react'; import { Button, Input, InputAddress, InputFile, InputNumber, TxButton } from '@polkadot/ui-app'; -import { compactAddLength, isHex } from '@polkadot/util'; +import { compactAddLength } from '@polkadot/util'; import { Hash } from '@polkadot/types'; import ABI from './ABI'; +import ValidateCode from './ValidateCode'; import store from './store'; import translate from './translate'; @@ -25,7 +26,7 @@ type State = { gasLimit: BN, isAbiValid: boolean, isBusy: boolean, - isHashValid: boolean, + isCodeValid: boolean, isNameValid: boolean, isNew: boolean, isWasmValid: boolean, @@ -39,7 +40,7 @@ class Deploy extends React.PureComponent { gasLimit: new BN(0), isAbiValid: true, isBusy: false, - isHashValid: false, + isCodeValid: false, isNew: true, isNameValid: false, isWasmValid: false @@ -88,10 +89,15 @@ class Deploy extends React.PureComponent { type='account' /> {this.renderInputName()} {this.renderInputAbi()} @@ -119,19 +125,23 @@ class Deploy extends React.PureComponent { private renderExisting () { const { t } = this.props; - const { codeHash, isAbiValid, isHashValid, isNameValid } = this.state; - const isValid = isAbiValid && isHashValid && isNameValid; + const { codeHash, isAbiValid, isCodeValid, isNameValid } = this.state; + const isValid = isAbiValid && isCodeValid && isNameValid; return ( <> + {this.renderInputName()} {this.renderInputAbi()} @@ -180,7 +190,9 @@ class Deploy extends React.PureComponent { } private onAddWasm = (wasm: Uint8Array, name: string): void => { - this.setState({ wasm: compactAddLength(wasm), isWasmValid: true }); + const isWasmValid = wasm.subarray(0, 4).toString() === '0,97,115,109'; // '\0asm' + + this.setState({ wasm: compactAddLength(wasm), isWasmValid }); this.onChangeName(name); } @@ -193,13 +205,17 @@ class Deploy extends React.PureComponent { } private onChangeHash = (codeHash: string): void => { - this.setState({ codeHash, isHashValid: isHex(codeHash) && codeHash.length === 66 }); + this.setState({ codeHash, isCodeValid: false }); } private onChangeName = (name: string): void => { this.setState({ name, isNameValid: name.length !== 0 }); } + private onValidateCode = (isCodeValid: boolean): void => { + this.setState({ isCodeValid }); + } + private toggleBusy = (): void => { this.setState(({ isBusy }) => ({ isBusy: !isBusy @@ -211,9 +227,9 @@ class Deploy extends React.PureComponent { abi: null, codeHash: null, isAbiValid: true, - isHashValid: false, + isCodeValid: false, isNameValid: false, - name: null, + name: '', isNew: !isNew })); } diff --git a/packages/app-contracts/src/Instantiate.tsx b/packages/app-contracts/src/Instantiate.tsx index cc40d1f7db..9a925a0e43 100644 --- a/packages/app-contracts/src/Instantiate.tsx +++ b/packages/app-contracts/src/Instantiate.tsx @@ -13,9 +13,9 @@ import { AccountId, ContractAbi } from '@polkadot/types'; import ABI from './ABI'; import Params from './Params'; +import ValidateAddr from './ValidateAddr'; import store from './store'; import translate from './translate'; -import keyring from '@polkadot/ui-keyring'; type Props = ComponentProps & I18nProps; @@ -197,6 +197,10 @@ class Create extends React.PureComponent { onChange={this.onChangeAddress} value={address} /> + {this.renderInputName()} {this.renderInputAbi()} @@ -259,17 +263,7 @@ class Create extends React.PureComponent { } private onChangeAddress = (address: string): void => { - let isAddressValid = false; - - try { - keyring.decodeAddress(address); - - isAddressValid = true; - } catch (error) { - // ignore - } - - this.setState({ address, isAddressValid }); + this.setState({ address, isAddressValid: false }); } private onChangeCode = (codeHash: string): void => { @@ -304,6 +298,10 @@ class Create extends React.PureComponent { this.setState({ params }); } + private onValidateAddr = (isAddressValid: boolean): void => { + this.setState({ isAddressValid }); + } + private toggleBusy = (): void => { this.setState(({ isBusy }) => ({ isBusy: !isBusy @@ -318,7 +316,7 @@ class Create extends React.PureComponent { isAbiValid: false, isNameValid: false, isNew: !isNew, - name: null + name: '' })); } diff --git a/packages/app-contracts/src/ValidateAddr.tsx b/packages/app-contracts/src/ValidateAddr.tsx new file mode 100644 index 0000000000..162215080c --- /dev/null +++ b/packages/app-contracts/src/ValidateAddr.tsx @@ -0,0 +1,83 @@ +// Copyright 2017-2019 @polkadot/app-contracts authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { I18nProps } from '@polkadot/ui-app/types'; +import { ApiProps } from '@polkadot/ui-api/types'; + +import React from 'react'; +import { CodeHash, Option } from '@polkadot/types'; +import { withCalls } from '@polkadot/ui-api'; +import { InfoForInput } from '@polkadot/ui-app'; +import keyring from '@polkadot/ui-keyring'; + +import translate from './translate'; + +type Props = ApiProps & I18nProps & { + address?: string | null, + contract_codeHashOf?: Option, + onChange: (isValid: boolean) => void +}; + +type State = { + isStored: boolean, + isValidAddr: boolean, + isValid: boolean +}; + +class ValidateAddr extends React.PureComponent { + state: State = { + isStored: false, + isValidAddr: false, + isValid: false + }; + + static getDerivedStateFromProps ({ address, contract_codeHashOf, onChange }: Props): State { + let isValidAddr = false; + + try { + keyring.decodeAddress(address || ''); + + isValidAddr = true; + } catch (error) { + // ignore + } + + const isStored = !!contract_codeHashOf && contract_codeHashOf.isSome; + const isValid = isValidAddr && isStored; + + // FIXME Really not convinced this is the correct place to do this type of callback? + onChange(isValid); + + return { + isStored, + isValidAddr, + isValid + }; + } + + render () { + const { t } = this.props; + const { isValid, isValidAddr } = this.state; + + if (isValid || !isValidAddr) { + return null; + } + + return ( + + { + isValidAddr + ? t('Unable to find deployed contract code at the specified address') + : t('The value is not in a valid address format') + } + + ); + } +} + +export default translate( + withCalls( + ['query.contract.codeHashOf', { paramName: 'address' }] + )(ValidateAddr) +); diff --git a/packages/app-contracts/src/ValidateCode.tsx b/packages/app-contracts/src/ValidateCode.tsx new file mode 100644 index 0000000000..1ebb7bc551 --- /dev/null +++ b/packages/app-contracts/src/ValidateCode.tsx @@ -0,0 +1,74 @@ +// Copyright 2017-2019 @polkadot/app-contracts authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import { I18nProps } from '@polkadot/ui-app/types'; +import { ApiProps } from '@polkadot/ui-api/types'; + +import React from 'react'; +import { Option, PrefabWasmModule } from '@polkadot/types'; +import { withCalls } from '@polkadot/ui-api'; +import { InfoForInput } from '@polkadot/ui-app'; +import { isHex } from '@polkadot/util'; + +import translate from './translate'; + +type Props = ApiProps & I18nProps & { + codeHash?: string | null, + contract_codeStorage?: Option, + onChange: (isValid: boolean) => void +}; + +type State = { + isStored: boolean, + isValidHex: boolean, + isValid: boolean +}; + +class ValidateCode extends React.PureComponent { + state: State = { + isStored: false, + isValidHex: false, + isValid: false + }; + + static getDerivedStateFromProps ({ codeHash, contract_codeStorage, onChange }: Props): State { + const isValidHex = !!codeHash && isHex(codeHash) && codeHash.length === 66; + const isStored = !!contract_codeStorage && contract_codeStorage.isSome; + const isValid = isValidHex && isStored; + + // FIXME Really not convinced this is the correct place to do this type of callback? + onChange(isValid); + + return { + isStored, + isValidHex, + isValid + }; + } + + render () { + const { t } = this.props; + const { isValid, isValidHex } = this.state; + + if (isValid || !isValidHex) { + return null; + } + + return ( + + { + isValidHex + ? t('Unable to find on-chain WASM code for the supplied codeHash') + : t('The codeHash is not a valid hex hash') + } + + ); + } +} + +export default translate( + withCalls( + ['query.contract.codeStorage', { paramName: 'codeHash' }] + )(ValidateCode) +); diff --git a/packages/ui-app/src/InfoForInput.tsx b/packages/ui-app/src/InfoForInput.tsx new file mode 100644 index 0000000000..680b8ecbf2 --- /dev/null +++ b/packages/ui-app/src/InfoForInput.tsx @@ -0,0 +1,48 @@ +// Copyright 2017-2019 @polkadot/ui-app authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import React from 'react'; +import styled from 'styled-components'; + +import { classes } from './util'; +import Labelled from './Labelled'; + +type Props = { + children: React.ReactNode + className?: string, + type?: 'error' | 'info' | 'warning'; +}; + +const Wrapper = styled.div` + background: white; + border-radius: 0 0 0.25rem 0.25rem; + margin: -0.5rem 0 0.25rem; + padding: 1.25rem 1.5rem 1rem; + + &.error { + background: #db2828; + color: #eee; + } + + &.warning { + background: #ffffe0; + } + + > ul { + margin: 0; + padding: 0; + } +`; + +export default class InfoForInput extends React.PureComponent { + render () { + const { children, className, type = 'info' } = this.props; + + return ( + + {children} + + ); + } +} diff --git a/packages/ui-app/src/Status/types.ts b/packages/ui-app/src/Status/types.ts index 1ed8bc2807..34e41525e3 100644 --- a/packages/ui-app/src/Status/types.ts +++ b/packages/ui-app/src/Status/types.ts @@ -37,7 +37,7 @@ export type QueueTx = AccountInfo & { rpc: RpcMethod, signerCb?: SignerCallback, signerOptions?: SignatureOptions, - txFailedCb?: TxCallback, + txFailedCb?: (status: SubmittableResult | null) => void, txSuccessCb?: TxCallback, txUpdateCb?: TxCallback, values?: Array, diff --git a/packages/ui-app/src/index.tsx b/packages/ui-app/src/index.tsx index d92fd4c0bb..517fc3abe3 100644 --- a/packages/ui-app/src/index.tsx +++ b/packages/ui-app/src/index.tsx @@ -18,6 +18,7 @@ export { default as Event } from './Event'; export { default as HelpOverlay } from './HelpOverlay'; export { default as Icon } from './Icon'; export { default as IdentityIcon } from './IdentityIcon'; +export { default as InfoForInput } from './InfoForInput'; export { default as Input } from './Input'; export { default as InputAddress } from './InputAddress'; export { default as InputBalance } from './InputBalance'; diff --git a/packages/ui-signer/src/Modal.tsx b/packages/ui-signer/src/Modal.tsx index 213d0dba1f..538426e827 100644 --- a/packages/ui-signer/src/Modal.tsx +++ b/packages/ui-signer/src/Modal.tsx @@ -229,13 +229,17 @@ class Signer extends React.PureComponent { return; } - const { id, signerCb } = currentItem; + const { id, signerCb, txFailedCb } = currentItem; queueSetTxStatus(id, 'cancelled'); if (isFunction(signerCb)) { signerCb(id, false); } + + if (isFunction(txFailedCb)) { + txFailedCb(null); + } } private onSend = async (): Promise => { @@ -352,6 +356,10 @@ class Signer extends React.PureComponent { } catch (error) { console.error('makeExtrinsicCall: error:', error.message); queueSetTxStatus(id, 'error', {}, error); + + if (isFunction(txFailedCb)) { + txFailedCb(null); + } } }