Allow setting of minBlock on sending (#3921)

* minBlock value formatting

* Allow Contract execute to specify minBock

* Transfer allows minBlock

* Cleanups

* Check errors, verify via testing

* Display Submitted/Submission block in MethodDecoding
This commit is contained in:
Jaco Greeff 2016-12-23 15:31:19 +01:00 committed by GitHub
parent 74efb22230
commit fc620d0d3e
16 changed files with 463 additions and 187 deletions

View File

@ -137,6 +137,10 @@ export function inOptions (options) {
options[key] = inNumber16((new BigNumber(options[key])).round()); options[key] = inNumber16((new BigNumber(options[key])).round());
break; break;
case 'minBlock':
options[key] = options[key] ? inNumber16(options[key]) : null;
break;
case 'value': case 'value':
case 'nonce': case 'nonce':
options[key] = inNumber16(options[key]); options[key] = inNumber16(options[key]);

View File

@ -204,7 +204,7 @@ describe('api/format/input', () => {
}); });
}); });
['gas', 'gasPrice', 'value', 'nonce'].forEach((input) => { ['gas', 'gasPrice', 'value', 'minBlock', 'nonce'].forEach((input) => {
it(`formats ${input} number as hexnumber`, () => { it(`formats ${input} number as hexnumber`, () => {
const block = {}; const block = {};
block[input] = 0x123; block[input] = 0x123;
@ -214,6 +214,10 @@ describe('api/format/input', () => {
}); });
}); });
it('passes minBlock as null when specified as such', () => {
expect(inOptions({ minBlock: null })).to.deep.equal({ minBlock: null });
});
it('ignores and passes through unknown keys', () => { it('ignores and passes through unknown keys', () => {
expect(inOptions({ someRandom: 'someRandom' })).to.deep.equal({ someRandom: 'someRandom' }); expect(inOptions({ someRandom: 'someRandom' })).to.deep.equal({ someRandom: 'someRandom' });
}); });

View File

@ -205,6 +205,10 @@ export function outTransaction (tx) {
tx[key] = outNumber(tx[key]); tx[key] = outNumber(tx[key]);
break; break;
case 'minBlock':
tx[key] = tx[key] ? outNumber(tx[key]) : null;
break;
case 'creates': case 'creates':
case 'from': case 'from':
case 'to': case 'to':

View File

@ -283,7 +283,7 @@ describe('api/format/output', () => {
}); });
}); });
['blockNumber', 'gasPrice', 'gas', 'nonce', 'transactionIndex', 'value'].forEach((input) => { ['blockNumber', 'gasPrice', 'gas', 'minBlock', 'nonce', 'transactionIndex', 'value'].forEach((input) => {
it(`formats ${input} number as hexnumber`, () => { it(`formats ${input} number as hexnumber`, () => {
const block = {}; const block = {};
block[input] = 0x123; block[input] = 0x123;
@ -294,6 +294,10 @@ describe('api/format/output', () => {
}); });
}); });
it('passes minBlock as null when null', () => {
expect(outTransaction({ minBlock: null })).to.deep.equal({ minBlock: null });
});
it('ignores and passes through unknown keys', () => { it('ignores and passes through unknown keys', () => {
expect(outTransaction({ someRandom: 'someRandom' })).to.deep.equal({ someRandom: 'someRandom' }); expect(outTransaction({ someRandom: 'someRandom' })).to.deep.equal({ someRandom: 'someRandom' });
}); });

View File

@ -0,0 +1,57 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Input, GasPriceEditor } from '~/ui';
import styles from '../executeContract.css';
export default class AdvancedStep extends Component {
static propTypes = {
gasStore: PropTypes.object.isRequired,
minBlock: PropTypes.string,
minBlockError: PropTypes.string,
onMinBlockChange: PropTypes.func
};
render () {
const { gasStore, minBlock, minBlockError, onMinBlockChange } = this.props;
return (
<div>
<Input
error={ minBlockError }
hint={
<FormattedMessage
id='executeContract.advanced.minBlock.hint'
defaultMessage='Only post the transaction after this block' />
}
label={
<FormattedMessage
id='executeContract.advanced.minBlock.label'
defaultMessage='BlockNumber to send from' />
}
value={ minBlock }
onSubmit={ onMinBlockChange } />
<div className={ styles.gaseditor }>
<GasPriceEditor store={ gasStore } />
</div>
</div>
);
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './advancedStep';

View File

@ -14,8 +14,9 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { Checkbox, MenuItem } from 'material-ui'; import { Checkbox, MenuItem } from 'material-ui';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { AddressSelect, Form, Input, Select, TypedInput } from '~/ui'; import { AddressSelect, Form, Input, Select, TypedInput } from '~/ui';
@ -29,29 +30,28 @@ const CHECK_STYLE = {
export default class DetailsStep extends Component { export default class DetailsStep extends Component {
static propTypes = { static propTypes = {
advancedOptions: PropTypes.bool,
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
contract: PropTypes.object.isRequired,
onAmountChange: PropTypes.func.isRequired,
onFromAddressChange: PropTypes.func.isRequired,
onValueChange: PropTypes.func.isRequired,
values: PropTypes.array.isRequired,
valuesError: PropTypes.array.isRequired,
amount: PropTypes.string, amount: PropTypes.string,
amountError: PropTypes.string, amountError: PropTypes.string,
balances: PropTypes.object, balances: PropTypes.object,
contract: PropTypes.object.isRequired,
fromAddress: PropTypes.string, fromAddress: PropTypes.string,
fromAddressError: PropTypes.string, fromAddressError: PropTypes.string,
func: PropTypes.object, func: PropTypes.object,
funcError: PropTypes.string, funcError: PropTypes.string,
gasEdit: PropTypes.bool, onAdvancedClick: PropTypes.func,
onAmountChange: PropTypes.func.isRequired,
onFromAddressChange: PropTypes.func.isRequired,
onFuncChange: PropTypes.func, onFuncChange: PropTypes.func,
onGasEditClick: PropTypes.func, onValueChange: PropTypes.func.isRequired,
values: PropTypes.array.isRequired,
valuesError: PropTypes.array.isRequired,
warning: PropTypes.string warning: PropTypes.string
} }
render () { render () {
const { accounts, amount, amountError, balances, fromAddress, fromAddressError, gasEdit, onGasEditClick, onFromAddressChange, onAmountChange } = this.props; const { accounts, advancedOptions, amount, amountError, balances, fromAddress, fromAddressError, onAdvancedClick, onAmountChange, onFromAddressChange } = this.props;
return ( return (
<Form> <Form>
@ -60,8 +60,16 @@ export default class DetailsStep extends Component {
accounts={ accounts } accounts={ accounts }
balances={ balances } balances={ balances }
error={ fromAddressError } error={ fromAddressError }
hint='the account to transact with' hint={
label='from account' <FormattedMessage
id='executeContract.details.address.label'
defaultMessage='the account to transact with' />
}
label={
<FormattedMessage
id='executeContract.details.address.hint'
defaultMessage='from account' />
}
onChange={ onFromAddressChange } onChange={ onFromAddressChange }
value={ fromAddress } /> value={ fromAddress } />
{ this.renderFunctionSelect() } { this.renderFunctionSelect() }
@ -70,16 +78,28 @@ export default class DetailsStep extends Component {
<div> <div>
<Input <Input
error={ amountError } error={ amountError }
hint='the amount to send to with the transaction' hint={
label='transaction value (in ETH)' <FormattedMessage
id='executeContract.details.amount.hint'
defaultMessage='the amount to send to with the transaction' />
}
label={
<FormattedMessage
id='executeContract.details.amount.label'
defaultMessage='transaction value (in ETH)' />
}
onSubmit={ onAmountChange } onSubmit={ onAmountChange }
value={ amount } /> value={ amount } />
</div> </div>
<div> <div>
<Checkbox <Checkbox
checked={ gasEdit } checked={ advancedOptions }
label='edit gas price or value' label={
onCheck={ onGasEditClick } <FormattedMessage
id='executeContract.details.advancedCheck.label'
defaultMessage='advanced sending options' />
}
onCheck={ onAdvancedClick }
style={ CHECK_STYLE } /> style={ CHECK_STYLE } />
</div> </div>
</div> </div>
@ -129,9 +149,17 @@ export default class DetailsStep extends Component {
return ( return (
<Select <Select
label='function to execute'
hint='the function to call on the contract'
error={ funcError } error={ funcError }
hint={
<FormattedMessage
id='executeContract.details.function.hint'
defaultMessage='the function to call on the contract' />
}
label={
<FormattedMessage
id='executeContract.details.function.label'
defaultMessage='function to execute' />
}
onChange={ this.onFuncChange } onChange={ this.onFuncChange }
value={ func.signature }> value={ func.signature }>
{ functions } { functions }

View File

@ -14,6 +14,19 @@
/* You should have received a copy of the GNU General Public License /* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.funcparams {
padding-left: 3em;
}
.gaseditor {
margin-top: 1em;
}
.paramname {
color: #aaa;
}
.modalbody, .modalbody,
.modalcenter { .modalcenter {
} }
@ -22,14 +35,6 @@
text-align: center; text-align: center;
} }
.funcparams {
padding-left: 3em;
}
.paramname {
color: #aaa;
}
.txhash { .txhash {
word-break: break-all; word-break: break-all;
} }

View File

@ -14,40 +14,54 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import { pick } from 'lodash';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { observer } from 'mobx-react';
import { pick } from 'lodash';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import { toWei } from '~/api/util/wei'; import { toWei } from '~/api/util/wei';
import { BusyStep, Button, CompletedStep, GasPriceEditor, IdentityIcon, Modal, TxHash } from '~/ui'; import { BusyStep, Button, CompletedStep, GasPriceEditor, IdentityIcon, Modal, TxHash } from '~/ui';
import { CancelIcon, DoneIcon, NextIcon, PrevIcon } from '~/ui/Icons';
import { MAX_GAS_ESTIMATION } from '~/util/constants'; import { MAX_GAS_ESTIMATION } from '~/util/constants';
import { validateAddress, validateUint } from '~/util/validation'; import { validateAddress, validateUint } from '~/util/validation';
import { parseAbiType } from '~/util/abi'; import { parseAbiType } from '~/util/abi';
import AdvancedStep from './AdvancedStep';
import DetailsStep from './DetailsStep'; import DetailsStep from './DetailsStep';
import { ERROR_CODES } from '~/api/transport/error'; import { ERROR_CODES } from '~/api/transport/error';
const STEP_DETAILS = 0; const STEP_DETAILS = 0;
const STEP_BUSY_OR_GAS = 1; const STEP_BUSY_OR_ADVANCED = 1;
const STEP_BUSY = 2; const STEP_BUSY = 2;
const TITLES = { const TITLES = {
transfer: 'function details', transfer:
sending: 'sending', <FormattedMessage
complete: 'complete', id='executeContract.steps.transfer'
gas: 'gas selection', defaultMessage='function details' />,
rejected: 'rejected' sending:
<FormattedMessage
id='executeContract.steps.sending'
defaultMessage='sending' />,
complete:
<FormattedMessage
id='executeContract.steps.complete'
defaultMessage='complete' />,
advanced:
<FormattedMessage
id='executeContract.steps.advanced'
defaultMessage='advanced options' />,
rejected:
<FormattedMessage
id='executeContract.steps.rejected'
defaultMessage='rejected' />
}; };
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete]; const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
const STAGES_GAS = [TITLES.transfer, TITLES.gas, TITLES.sending, TITLES.complete]; const STAGES_ADVANCED = [TITLES.transfer, TITLES.advanced, TITLES.sending, TITLES.complete];
@observer @observer
class ExecuteContract extends Component { class ExecuteContract extends Component {
@ -70,13 +84,15 @@ class ExecuteContract extends Component {
gasStore = new GasPriceEditor.Store(this.context.api, { gasLimit: this.props.gasLimit }); gasStore = new GasPriceEditor.Store(this.context.api, { gasLimit: this.props.gasLimit });
state = { state = {
advancedOptions: false,
amount: '0', amount: '0',
amountError: null, amountError: null,
busyState: null, busyState: null,
fromAddressError: null, fromAddressError: null,
func: null, func: null,
funcError: null, funcError: null,
gasEdit: false, minBlock: '0',
minBlockError: null,
rejected: false, rejected: false,
sending: false, sending: false,
step: STEP_DETAILS, step: STEP_DETAILS,
@ -101,8 +117,8 @@ class ExecuteContract extends Component {
} }
render () { render () {
const { sending, step, gasEdit, rejected } = this.state; const { advancedOptions, rejected, sending, step } = this.state;
const steps = gasEdit ? STAGES_GAS : STAGES_BASIC; const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
if (rejected) { if (rejected) {
steps[steps.length - 1] = TITLES.rejected; steps[steps.length - 1] = TITLES.rejected;
@ -115,7 +131,7 @@ class ExecuteContract extends Component {
current={ step } current={ step }
steps={ steps } steps={ steps }
visible visible
waiting={ gasEdit ? [STEP_BUSY] : [STEP_BUSY_OR_GAS] }> waiting={ advancedOptions ? [STEP_BUSY] : [STEP_BUSY_OR_ADVANCED] }>
{ this.renderStep() } { this.renderStep() }
</Modal> </Modal>
); );
@ -123,20 +139,28 @@ class ExecuteContract extends Component {
renderDialogActions () { renderDialogActions () {
const { onClose, fromAddress } = this.props; const { onClose, fromAddress } = this.props;
const { gasEdit, sending, step, fromAddressError, valuesError } = this.state; const { advancedOptions, sending, step, fromAddressError, minBlockError, valuesError } = this.state;
const hasError = fromAddressError || valuesError.find((error) => error); const hasError = fromAddressError || minBlockError || valuesError.find((error) => error);
const cancelBtn = ( const cancelBtn = (
<Button <Button
key='cancel' key='cancel'
label='Cancel' label={
icon={ <ContentClear /> } <FormattedMessage
id='executeContract.button.cancel'
defaultMessage='cancel' />
}
icon={ <CancelIcon /> }
onClick={ onClose } /> onClick={ onClose } />
); );
const postBtn = ( const postBtn = (
<Button <Button
key='postTransaction' key='postTransaction'
label='post transaction' label={
<FormattedMessage
id='executeContract.button.post'
defaultMessage='post transaction' />
}
disabled={ !!(sending || hasError) } disabled={ !!(sending || hasError) }
icon={ <IdentityIcon address={ fromAddress } button /> } icon={ <IdentityIcon address={ fromAddress } button /> }
onClick={ this.postTransaction } /> onClick={ this.postTransaction } />
@ -144,28 +168,36 @@ class ExecuteContract extends Component {
const nextBtn = ( const nextBtn = (
<Button <Button
key='nextStep' key='nextStep'
label='next' label={
icon={ <NavigationArrowForward /> } <FormattedMessage
id='executeContract.button.next'
defaultMessage='next' />
}
icon={ <NextIcon /> }
onClick={ this.onNextClick } /> onClick={ this.onNextClick } />
); );
const prevBtn = ( const prevBtn = (
<Button <Button
key='prevStep' key='prevStep'
label='prev' label={
icon={ <NavigationArrowBack /> } <FormattedMessage
id='executeContract.button.prev'
defaultMessage='prev' />
}
icon={ <PrevIcon /> }
onClick={ this.onPrevClick } /> onClick={ this.onPrevClick } />
); );
if (step === STEP_DETAILS) { if (step === STEP_DETAILS) {
return [ return [
cancelBtn, cancelBtn,
gasEdit ? nextBtn : postBtn advancedOptions ? nextBtn : postBtn
]; ];
} else if (step === (gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS)) { } else if (step === (advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED)) {
return [ return [
cancelBtn cancelBtn
]; ];
} else if (gasEdit && (step === STEP_BUSY_OR_GAS)) { } else if (advancedOptions && (step === STEP_BUSY_OR_ADVANCED)) {
return [ return [
cancelBtn, cancelBtn,
prevBtn, prevBtn,
@ -176,23 +208,34 @@ class ExecuteContract extends Component {
return [ return [
<Button <Button
key='close' key='close'
label='Done' label={
icon={ <ActionDoneAll /> } <FormattedMessage
id='executeContract.button.done'
defaultMessage='done' />
}
icon={ <DoneIcon /> }
onClick={ onClose } /> onClick={ onClose } />
]; ];
} }
renderStep () { renderStep () {
const { onFromAddressChange } = this.props; const { onFromAddressChange } = this.props;
const { gasEdit, step, busyState, txhash, rejected } = this.state; const { advancedOptions, step, busyState, minBlock, minBlockError, txhash, rejected } = this.state;
const { errorEstimated } = this.gasStore; const { errorEstimated } = this.gasStore;
if (rejected) { if (rejected) {
return ( return (
<BusyStep <BusyStep
title='The execution has been rejected' title={
state='You can safely close this window, the function execution will not occur.' <FormattedMessage
/> id='executeContract.rejected.title'
defaultMessage='The execution has been rejected' />
}
state={
<FormattedMessage
id='executeContract.rejected.state'
defaultMessage='You can safely close this window, the function execution will not occur.' />
} />
); );
} }
@ -205,19 +248,26 @@ class ExecuteContract extends Component {
onAmountChange={ this.onAmountChange } onAmountChange={ this.onAmountChange }
onFromAddressChange={ onFromAddressChange } onFromAddressChange={ onFromAddressChange }
onFuncChange={ this.onFuncChange } onFuncChange={ this.onFuncChange }
onGasEditClick={ this.onGasEditClick } onAdvancedClick={ this.onAdvancedClick }
onValueChange={ this.onValueChange } /> onValueChange={ this.onValueChange } />
); );
} else if (step === (gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS)) { } else if (step === (advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED)) {
return ( return (
<BusyStep <BusyStep
title='The function execution is in progress' title={
<FormattedMessage
id='executeContract.busy.title'
defaultMessage='The function execution is in progress' />
}
state={ busyState } /> state={ busyState } />
); );
} else if (gasEdit && (step === STEP_BUSY_OR_GAS)) { } else if (advancedOptions && (step === STEP_BUSY_OR_ADVANCED)) {
return ( return (
<GasPriceEditor <AdvancedStep
store={ this.gasStore } /> gasStore={ this.gasStore }
minBlock={ minBlock }
minBlockError={ minBlockError }
onMinBlockChange={ this.onMinBlockChange } />
); );
} }
@ -245,6 +295,15 @@ class ExecuteContract extends Component {
}, this.estimateGas); }, this.estimateGas);
} }
onMinBlockChange = (minBlock) => {
const minBlockError = validateUint(minBlock).valueError;
this.setState({
minBlock,
minBlockError
});
}
onValueChange = (event, index, _value) => { onValueChange = (event, index, _value) => {
const { func, values, valuesError } = this.state; const { func, values, valuesError } = this.state;
const input = func.inputs.find((input, _index) => index === _index); const input = func.inputs.find((input, _index) => index === _index);
@ -305,22 +364,28 @@ class ExecuteContract extends Component {
postTransaction = () => { postTransaction = () => {
const { api, store } = this.context; const { api, store } = this.context;
const { fromAddress } = this.props; const { fromAddress } = this.props;
const { amount, func, gasEdit, values } = this.state; const { advancedOptions, amount, func, minBlock, values } = this.state;
const steps = gasEdit ? STAGES_GAS : STAGES_BASIC; const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
const finalstep = steps.length - 1; const finalstep = steps.length - 1;
const options = { const options = {
gas: this.gasStore.gas, gas: this.gasStore.gas,
gasPrice: this.gasStore.price, gasPrice: this.gasStore.price,
from: fromAddress, from: fromAddress,
minBlock: new BigNumber(minBlock || 0).gt(0) ? minBlock : null,
value: api.util.toWei(amount || 0) value: api.util.toWei(amount || 0)
}; };
this.setState({ sending: true, step: gasEdit ? STEP_BUSY : STEP_BUSY_OR_GAS }); this.setState({ sending: true, step: advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED });
func func
.postTransaction(options, values) .postTransaction(options, values)
.then((requestId) => { .then((requestId) => {
this.setState({ busyState: 'Waiting for authorization in the Parity Signer' }); this.setState({
busyState:
<FormattedMessage
id='executeContract.busy.waitAuth'
defaultMessage='Waiting for authorization in the Parity Signer' />
});
return api return api
.pollMethod('parity_checkRequest', requestId) .pollMethod('parity_checkRequest', requestId)
@ -334,7 +399,15 @@ class ExecuteContract extends Component {
}); });
}) })
.then((txhash) => { .then((txhash) => {
this.setState({ sending: false, step: finalstep, txhash, busyState: 'Your transaction has been posted to the network' }); this.setState({
sending: false,
step: finalstep,
txhash,
busyState:
<FormattedMessage
id='executeContract.busy.posted'
defaultMessage='Your transaction has been posted to the network' />
});
}) })
.catch((error) => { .catch((error) => {
console.error('postTransaction', error); console.error('postTransaction', error);
@ -342,9 +415,9 @@ class ExecuteContract extends Component {
}); });
} }
onGasEditClick = () => { onAdvancedClick = () => {
this.setState({ this.setState({
gasEdit: !this.state.gasEdit advancedOptions: !this.state.advancedOptions
}); });
} }

View File

@ -15,29 +15,50 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { GasPriceEditor, Form, Input } from '~/ui'; import { GasPriceEditor, Form, Input } from '~/ui';
import styles from '../transfer.css';
export default class Extras extends Component { export default class Extras extends Component {
static propTypes = { static propTypes = {
isEth: PropTypes.bool,
data: PropTypes.string, data: PropTypes.string,
dataError: PropTypes.string, dataError: PropTypes.string,
total: PropTypes.string, gasStore: PropTypes.object.isRequired,
totalError: PropTypes.string, isEth: PropTypes.bool,
minBlock: PropTypes.string,
minBlockError: PropTypes.string,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
gasStore: PropTypes.object.isRequired total: PropTypes.string,
totalError: PropTypes.string
} }
render () { render () {
const { gasStore, onChange } = this.props; const { gasStore, minBlock, minBlockError, onChange } = this.props;
return ( return (
<Form> <Form>
{ this.renderData() } { this.renderData() }
<Input
error={ minBlockError }
hint={
<FormattedMessage
id='executeContract.advanced.minBlock.hint'
defaultMessage='Only post the transaction after this block' />
}
label={
<FormattedMessage
id='executeContract.advanced.minBlock.label'
defaultMessage='BlockNumber to send from' />
}
value={ minBlock }
onChange={ this.onEditMinBlock } />
<div className={ styles.gaseditor }>
<GasPriceEditor <GasPriceEditor
store={ gasStore } store={ gasStore }
onChange={ onChange } /> onChange={ onChange } />
</div>
</Form> </Form>
); );
} }
@ -50,18 +71,28 @@ export default class Extras extends Component {
} }
return ( return (
<div>
<Input <Input
hint='the data to pass through with the transaction'
label='transaction data'
value={ data }
error={ dataError } error={ dataError }
onChange={ this.onEditData } /> hint={
</div> <FormattedMessage
id='transfer.advanced.data.hint'
defaultMessage='the data to pass through with the transaction' />
}
label={
<FormattedMessage
id='transfer.advanced.data.label'
defaultMessage='transaction data' />
}
onChange={ this.onEditData }
value={ data } />
); );
} }
onEditData = (event) => { onEditData = (event) => {
this.props.onChange('data', event.target.value); this.props.onChange('data', event.target.value);
} }
onEditMinBlock = (event) => {
this.props.onChange('minBlock', event.target.value);
}
} }

View File

@ -49,6 +49,9 @@ export default class TransferStore {
@observable data = ''; @observable data = '';
@observable dataError = null; @observable dataError = null;
@observable minBlock = '0';
@observable minBlockError = null;
@observable recipient = ''; @observable recipient = '';
@observable recipientError = ERRORS.requireRecipient; @observable recipientError = ERRORS.requireRecipient;
@ -84,7 +87,7 @@ export default class TransferStore {
@computed get isValid () { @computed get isValid () {
const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError; const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError;
const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.totalError; const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.minBlockError && !this.totalError;
const verifyValid = !this.passwordError; const verifyValid = !this.passwordError;
switch (this.stage) { switch (this.stage) {
@ -92,7 +95,9 @@ export default class TransferStore {
return detailsValid; return detailsValid;
case 1: case 1:
return this.extras ? extrasValid : verifyValid; return this.extras
? extrasValid
: verifyValid;
case 2: case 2:
return verifyValid; return verifyValid;
@ -155,6 +160,9 @@ export default class TransferStore {
case 'gasPrice': case 'gasPrice':
return this._onUpdateGasPrice(value); return this._onUpdateGasPrice(value);
case 'minBlock':
return this._onUpdateMinBlock(value);
case 'recipient': case 'recipient':
return this._onUpdateRecipient(value); return this._onUpdateRecipient(value);
@ -254,6 +262,14 @@ export default class TransferStore {
this.recalculate(); this.recalculate();
} }
@action _onUpdateMinBlock = (minBlock) => {
console.log('minBlock', minBlock);
transaction(() => {
this.minBlock = minBlock;
this.minBlockError = this._validatePositiveNumber(minBlock);
});
}
@action _onUpdateGasPrice = (gasPrice) => { @action _onUpdateGasPrice = (gasPrice) => {
this.recalculate(); this.recalculate();
} }
@ -412,6 +428,9 @@ export default class TransferStore {
send () { send () {
const { options, values } = this._getTransferParams(); const { options, values } = this._getTransferParams();
options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null;
return this._getTransferMethod().postTransaction(options, values); return this._getTransferMethod().postTransaction(options, values);
} }

View File

@ -15,65 +15,68 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.columns {
display: flex;
position: relative;
flex-wrap: wrap;
&>div {
flex: 0 1 50%;
position: relative;
width: 50%;
}
}
.gaseditor {
margin-top: 1em;
}
.info { .info {
line-height: 1.618em; line-height: 1.618em;
width: 100%; width: 100%;
} }
.columns {
display: flex;
flex-wrap: wrap;
position: relative;
}
.row { .row {
display: flex; display: flex;
flex-wrap: wrap;
position: relative;
flex-direction: column; flex-direction: column;
} flex-wrap: wrap;
.columns>div {
flex: 0 1 50%;
width: 50%;
position: relative; position: relative;
} }
.floatbutton { .floatbutton {
text-align: right;
float: right; float: right;
margin-left: -100%; margin-left: -100%;
margin-top: 28px; margin-top: 28px;
} text-align: right;
.floatbutton>div { &>div {
margin-right: 0.5em; margin-right: 0.5em;
} }
.tokenSelect {
} }
.token { .token {
height: 32px; height: 32px;
padding: 4px 0; padding: 4px 0;
}
.tokenSelect .token { img {
margin-top: 10px;
}
.token img {
height: 32px; height: 32px;
width: 32px; width: 32px;
margin: 0 16px 0 0; margin: 0 16px 0 0;
z-index: 10; z-index: 10;
} }
.token div { div {
height: 32px; height: 32px;
line-height: 32px; line-height: 32px;
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
}
}
.tokenSelect {
.token {
margin-top: 10px;
}
} }
.tokenbalance { .tokenbalance {

View File

@ -20,13 +20,9 @@ import { bindActionCreators } from 'redux';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { pick } from 'lodash'; import { pick } from 'lodash';
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
import { newError } from '~/ui/Errors/actions';
import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash, Input } from '~/ui'; import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash, Input } from '~/ui';
import { newError } from '~/ui/Errors/actions';
import { CancelIcon, DoneIcon, NextIcon, PrevIcon } from '~/ui/Icons';
import { nullableProptype } from '~/util/proptypes'; import { nullableProptype } from '~/util/proptypes';
import Details from './Details'; import Details from './Details';
@ -188,17 +184,19 @@ class Transfer extends Component {
return null; return null;
} }
const { isEth, data, dataError, total, totalError } = this.store; const { isEth, data, dataError, minBlock, minBlockError, total, totalError } = this.store;
return ( return (
<Extras <Extras
isEth={ isEth }
data={ data } data={ data }
dataError={ dataError } dataError={ dataError }
total={ total }
totalError={ totalError }
gasStore={ this.store.gasStore } gasStore={ this.store.gasStore }
onChange={ this.store.onUpdateDetails } /> isEth={ isEth }
minBlock={ minBlock }
minBlockError={ minBlockError }
onChange={ this.store.onUpdateDetails }
total={ total }
totalError={ totalError } />
); );
} }
@ -208,20 +206,20 @@ class Transfer extends Component {
const cancelBtn = ( const cancelBtn = (
<Button <Button
icon={ <ContentClear /> } icon={ <CancelIcon /> }
label='Cancel' label='Cancel'
onClick={ this.handleClose } /> onClick={ this.handleClose } />
); );
const nextBtn = ( const nextBtn = (
<Button <Button
disabled={ !this.store.isValid } disabled={ !this.store.isValid }
icon={ <NavigationArrowForward /> } icon={ <NextIcon /> }
label='Next' label='Next'
onClick={ this.store.onNext } /> onClick={ this.store.onNext } />
); );
const prevBtn = ( const prevBtn = (
<Button <Button
icon={ <NavigationArrowBack /> } icon={ <PrevIcon /> }
label='Back' label='Back'
onClick={ this.store.onPrev } /> onClick={ this.store.onPrev } />
); );
@ -234,7 +232,7 @@ class Transfer extends Component {
); );
const doneBtn = ( const doneBtn = (
<Button <Button
icon={ <ActionDoneAll /> } icon={ <DoneIcon /> }
label='Close' label='Close'
onClick={ this.handleClose } /> onClick={ this.handleClose } />
); );

View File

@ -14,16 +14,18 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { MenuItem } from 'material-ui';
import { isEqual, pick } from 'lodash'; import { isEqual, pick } from 'lodash';
import { MenuItem } from 'material-ui';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { fromWei } from '~/api/util/wei';
import { nodeOrStringProptype } from '~/util/proptypes';
import AutoComplete from '../AutoComplete'; import AutoComplete from '../AutoComplete';
import IdentityIcon from '../../IdentityIcon'; import IdentityIcon from '../../IdentityIcon';
import IdentityName from '../../IdentityName'; import IdentityName from '../../IdentityName';
import { fromWei } from '~/api/util/wei';
import styles from './addressSelect.css'; import styles from './addressSelect.css';
export default class AddressSelect extends Component { export default class AddressSelect extends Component {
@ -40,9 +42,9 @@ export default class AddressSelect extends Component {
contacts: PropTypes.object, contacts: PropTypes.object,
contracts: PropTypes.object, contracts: PropTypes.object,
disabled: PropTypes.bool, disabled: PropTypes.bool,
error: PropTypes.string, error: nodeOrStringProptype(),
hint: PropTypes.string, hint: nodeOrStringProptype(),
label: PropTypes.string, label: nodeOrStringProptype(),
tokens: PropTypes.object, tokens: PropTypes.object,
value: PropTypes.string, value: PropTypes.string,
wallets: PropTypes.object wallets: PropTypes.object
@ -116,18 +118,27 @@ export default class AddressSelect extends Component {
<AutoComplete <AutoComplete
className={ !icon ? '' : styles.paddedInput } className={ !icon ? '' : styles.paddedInput }
disabled={ disabled } disabled={ disabled }
label={ label }
hint={ hint ? `search for ${hint}` : 'search for an address' }
error={ error }
onChange={ this.onChange }
onBlur={ this.onBlur }
onUpdateInput={ allowInput && this.onUpdateInput }
value={ searchText }
filter={ this.handleFilter }
entries={ autocompleteEntries } entries={ autocompleteEntries }
entry={ this.getEntry() || {} } entry={ this.getEntry() || {} }
error={ error }
filter={ this.handleFilter }
hint={
<FormattedMessage
id='ui.addressSelect.search.hint'
defaultMessage='search for {hint}'
values={ {
hint: hint ||
<FormattedMessage
id='ui.addressSelect.search.address'
defaultMessage='address' />
} } />
}
label={ label }
onBlur={ this.onBlur }
onChange={ this.onChange }
onUpdateInput={ allowInput && this.onUpdateInput }
renderItem={ this.renderItem } renderItem={ this.renderItem }
/> value={ searchText } />
{ icon } { icon }
</div> </div>
); );
@ -148,9 +159,10 @@ export default class AddressSelect extends Component {
return ( return (
<IdentityIcon <IdentityIcon
address={ value }
center
className={ classes.join(' ') } className={ classes.join(' ') }
inline center inline />
address={ value } />
); );
} }
@ -162,9 +174,10 @@ export default class AddressSelect extends Component {
if (!this.items[address] || this.items[address].balance !== balance) { if (!this.items[address] || this.items[address].balance !== balance) {
this.items[address] = { this.items[address] = {
address,
balance,
text: name && name.toUpperCase() || address, text: name && name.toUpperCase() || address,
value: this.renderMenuItem(address), value: this.renderMenuItem(address)
address, balance
}; };
} }
@ -189,7 +202,7 @@ export default class AddressSelect extends Component {
} }
renderBalance (address) { renderBalance (address) {
const balance = this.getBalance(address); const balance = this.getBalance(address) || 0;
const value = fromWei(balance); const value = fromWei(balance);
return ( return (
@ -207,12 +220,13 @@ export default class AddressSelect extends Component {
const item = ( const item = (
<div className={ styles.account }> <div className={ styles.account }>
<IdentityIcon <IdentityIcon
address={ address }
center
className={ styles.image } className={ styles.image }
inline center inline />
address={ address } />
<IdentityName <IdentityName
className={ styles.name } address={ address }
address={ address } /> className={ styles.name } />
{ balance } { balance }
</div> </div>
); );
@ -221,8 +235,8 @@ export default class AddressSelect extends Component {
<MenuItem <MenuItem
className={ styles.menuItem } className={ styles.menuItem }
key={ address } key={ address }
value={ address } label={ item }
label={ item }> value={ address }>
{ item } { item }
</MenuItem> </MenuItem>
); );

View File

@ -14,12 +14,13 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import keycode from 'keycode'; import keycode from 'keycode';
import { isEqual } from 'lodash';
import { MenuItem, AutoComplete as MUIAutoComplete, Divider as MUIDivider } from 'material-ui'; import { MenuItem, AutoComplete as MUIAutoComplete, Divider as MUIDivider } from 'material-ui';
import { PopoverAnimationVertical } from 'material-ui/Popover'; import { PopoverAnimationVertical } from 'material-ui/Popover';
import React, { Component, PropTypes } from 'react';
import { isEqual } from 'lodash'; import { nodeOrStringProptype } from '~/util/proptypes';
import styles from './autocomplete.css'; import styles from './autocomplete.css';
@ -41,21 +42,21 @@ class Divider extends Component {
export default class AutoComplete extends Component { export default class AutoComplete extends Component {
static propTypes = { static propTypes = {
onChange: PropTypes.func.isRequired,
onUpdateInput: PropTypes.func,
disabled: PropTypes.bool,
label: PropTypes.string,
hint: PropTypes.string,
error: PropTypes.string,
value: PropTypes.string,
className: PropTypes.string, className: PropTypes.string,
filter: PropTypes.func, disabled: PropTypes.bool,
renderItem: PropTypes.func,
entry: PropTypes.object, entry: PropTypes.object,
entries: PropTypes.oneOfType([ entries: PropTypes.oneOfType([
PropTypes.array, PropTypes.array,
PropTypes.object PropTypes.object
]) ]),
error: nodeOrStringProptype(),
filter: PropTypes.func,
hint: nodeOrStringProptype(),
label: nodeOrStringProptype(),
onChange: PropTypes.func.isRequired,
onUpdateInput: PropTypes.func,
renderItem: PropTypes.func,
value: PropTypes.string
}; };
state = { state = {

View File

@ -122,10 +122,24 @@ class MethodDecoding extends Component {
</span> </span>
<span> for a total transaction value of </span> <span> for a total transaction value of </span>
<span className={ styles.highlight }>{ this.renderEtherValue(gasValue) }</span> <span className={ styles.highlight }>{ this.renderEtherValue(gasValue) }</span>
{ this.renderMinBlock() }
</div> </div>
); );
} }
renderMinBlock () {
const { historic, transaction } = this.props;
const { minBlock } = transaction;
if (!minBlock || minBlock.eq(0)) {
return null;
}
return (
<span>, { historic ? 'Submitted' : 'Submission' } at block <span className={ styles.highlight }>#{ minBlock.toFormat(0) }</span></span>
);
}
renderAction () { renderAction () {
const { token } = this.props; const { token } = this.props;
const { methodName, methodInputs, methodSignature, isDeploy, isReceived, isContract } = this.state; const { methodName, methodInputs, methodSignature, isDeploy, isReceived, isContract } = this.state;