Add block & timestamp conditions to Signer (#4411)
* WIP * WIP (with lint) * Update ui/RadioButtons * transaction.condition * Date & Time selection in-place * Swap to condition-only * Fix tests, align naming * Pick error properly from validation * Update tests * condition: time sent withough ms * Format numbers as base-10 * override popup styles (zIndex) * Pass condition to signer * Update expectation (failing test typo) * Adjust min/max height for expanded bar * Fix address display * Fix name display * Number inputs for gas/gasPrice/blockNumber * Default blockNumber to 1 (align with min setting) * Update tests with min value * Add Block Number * Fix failing tests (after blockNumber intro)
This commit is contained in:
parent
312aa72747
commit
cd4d489b57
@ -127,6 +127,18 @@ export function inNumber16 (number) {
|
||||
return inHex(bn.toString(16));
|
||||
}
|
||||
|
||||
export function inOptionsCondition (condition) {
|
||||
if (condition) {
|
||||
if (condition.block) {
|
||||
condition.block = condition.block ? inNumber10(condition.block) : null;
|
||||
} else if (condition.time) {
|
||||
condition.time = inNumber10(Math.floor(condition.time.getTime() / 1000));
|
||||
}
|
||||
}
|
||||
|
||||
return condition;
|
||||
}
|
||||
|
||||
export function inOptions (options) {
|
||||
if (options) {
|
||||
Object.keys(options).forEach((key) => {
|
||||
@ -136,6 +148,10 @@ export function inOptions (options) {
|
||||
options[key] = inAddress(options[key]);
|
||||
break;
|
||||
|
||||
case 'condition':
|
||||
options[key] = inOptionsCondition(options[key]);
|
||||
break;
|
||||
|
||||
case 'gas':
|
||||
case 'gasPrice':
|
||||
options[key] = inNumber16((new BigNumber(options[key])).round());
|
||||
|
@ -221,6 +221,18 @@ export function outSyncing (syncing) {
|
||||
return syncing;
|
||||
}
|
||||
|
||||
export function outTransactionCondition (condition) {
|
||||
if (condition) {
|
||||
if (condition.block) {
|
||||
condition.block = outNumber(condition.block);
|
||||
} else if (condition.time) {
|
||||
condition.time = outDate(condition.time);
|
||||
}
|
||||
}
|
||||
|
||||
return condition;
|
||||
}
|
||||
|
||||
export function outTransaction (tx) {
|
||||
if (tx) {
|
||||
Object.keys(tx).forEach((key) => {
|
||||
@ -234,8 +246,14 @@ export function outTransaction (tx) {
|
||||
tx[key] = outNumber(tx[key]);
|
||||
break;
|
||||
|
||||
case 'condition':
|
||||
tx[key] = outTransactionCondition(tx[key]);
|
||||
break;
|
||||
|
||||
case 'minBlock':
|
||||
tx[key] = tx[key] ? outNumber(tx[key]) : null;
|
||||
tx[key] = tx[key]
|
||||
? outNumber(tx[key])
|
||||
: null;
|
||||
break;
|
||||
|
||||
case 'creates':
|
||||
|
@ -15,45 +15,22 @@
|
||||
// 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 { 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
|
||||
gasStore: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
render () {
|
||||
const { gasStore, minBlock, minBlockError, onMinBlockChange } = this.props;
|
||||
const { gasStore } = 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 className={ styles.gaseditor }>
|
||||
<GasPriceEditor store={ gasStore } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// 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';
|
||||
@ -100,8 +99,6 @@ class ExecuteContract extends Component {
|
||||
fromAddressError: null,
|
||||
func: null,
|
||||
funcError: null,
|
||||
minBlock: '0',
|
||||
minBlockError: null,
|
||||
rejected: false,
|
||||
sending: false,
|
||||
step: STEP_DETAILS,
|
||||
@ -167,8 +164,8 @@ class ExecuteContract extends Component {
|
||||
|
||||
renderDialogActions () {
|
||||
const { onClose, fromAddress } = this.props;
|
||||
const { advancedOptions, sending, step, fromAddressError, minBlockError, valuesError } = this.state;
|
||||
const hasError = fromAddressError || minBlockError || valuesError.find((error) => error);
|
||||
const { advancedOptions, sending, step, fromAddressError, valuesError } = this.state;
|
||||
const hasError = fromAddressError || valuesError.find((error) => error);
|
||||
|
||||
const cancelBtn = (
|
||||
<Button
|
||||
@ -258,7 +255,7 @@ class ExecuteContract extends Component {
|
||||
|
||||
renderStep () {
|
||||
const { onFromAddressChange } = this.props;
|
||||
const { advancedOptions, step, busyState, minBlock, minBlockError, txhash, rejected } = this.state;
|
||||
const { advancedOptions, step, busyState, txhash, rejected } = this.state;
|
||||
|
||||
if (rejected) {
|
||||
return (
|
||||
@ -305,12 +302,7 @@ class ExecuteContract extends Component {
|
||||
);
|
||||
} else if (advancedOptions && (step === STEP_BUSY_OR_ADVANCED)) {
|
||||
return (
|
||||
<AdvancedStep
|
||||
gasStore={ this.gasStore }
|
||||
minBlock={ minBlock }
|
||||
minBlockError={ minBlockError }
|
||||
onMinBlockChange={ this.onMinBlockChange }
|
||||
/>
|
||||
<AdvancedStep gasStore={ this.gasStore } />
|
||||
);
|
||||
}
|
||||
|
||||
@ -339,15 +331,6 @@ class ExecuteContract extends Component {
|
||||
}, this.estimateGas);
|
||||
}
|
||||
|
||||
onMinBlockChange = (minBlock) => {
|
||||
const minBlockError = validateUint(minBlock).valueError;
|
||||
|
||||
this.setState({
|
||||
minBlock,
|
||||
minBlockError
|
||||
});
|
||||
}
|
||||
|
||||
onValueChange = (event, index, _value) => {
|
||||
const { func, values, valuesError } = this.state;
|
||||
const input = func.inputs.find((input, _index) => index === _index);
|
||||
@ -409,17 +392,14 @@ class ExecuteContract extends Component {
|
||||
postTransaction = () => {
|
||||
const { api, store } = this.context;
|
||||
const { fromAddress } = this.props;
|
||||
const { advancedOptions, amount, func, minBlock, values } = this.state;
|
||||
const { advancedOptions, amount, func, values } = this.state;
|
||||
const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
|
||||
const finalstep = steps.length - 1;
|
||||
|
||||
const options = {
|
||||
gas: this.gasStore.gas,
|
||||
gasPrice: this.gasStore.price,
|
||||
const options = this.gasStore.overrideTransaction({
|
||||
from: fromAddress,
|
||||
minBlock: new BigNumber(minBlock || 0).gt(0) ? minBlock : null,
|
||||
value: api.util.toWei(amount || 0)
|
||||
};
|
||||
});
|
||||
|
||||
this.setState({ sending: true, step: advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED });
|
||||
|
||||
|
@ -27,36 +27,17 @@ export default class Extras extends Component {
|
||||
dataError: PropTypes.string,
|
||||
gasStore: PropTypes.object.isRequired,
|
||||
isEth: PropTypes.bool,
|
||||
minBlock: PropTypes.string,
|
||||
minBlockError: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
total: PropTypes.string,
|
||||
totalError: PropTypes.string
|
||||
}
|
||||
|
||||
render () {
|
||||
const { gasStore, minBlock, minBlockError, onChange } = this.props;
|
||||
const { gasStore, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
{ this.renderData() }
|
||||
<Input
|
||||
error={ minBlockError }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='transferModal.minBlock.hint'
|
||||
defaultMessage='Only post the transaction after this block'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='transferModal.minBlock.label'
|
||||
defaultMessage='BlockNumber to send from'
|
||||
/>
|
||||
}
|
||||
value={ minBlock }
|
||||
onChange={ this.onEditMinBlock }
|
||||
/>
|
||||
<div className={ styles.gaseditor }>
|
||||
<GasPriceEditor
|
||||
store={ gasStore }
|
||||
@ -98,8 +79,4 @@ export default class Extras extends Component {
|
||||
onEditData = (event) => {
|
||||
this.props.onChange('data', event.target.value);
|
||||
}
|
||||
|
||||
onEditMinBlock = (event) => {
|
||||
this.props.onChange('minBlock', event.target.value);
|
||||
}
|
||||
}
|
||||
|
@ -52,9 +52,6 @@ export default class TransferStore {
|
||||
@observable data = '';
|
||||
@observable dataError = null;
|
||||
|
||||
@observable minBlock = '0';
|
||||
@observable minBlockError = null;
|
||||
|
||||
@observable recipient = '';
|
||||
@observable recipientError = ERRORS.requireRecipient;
|
||||
|
||||
@ -78,39 +75,6 @@ export default class TransferStore {
|
||||
|
||||
gasStore = null;
|
||||
|
||||
@computed get steps () {
|
||||
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
|
||||
|
||||
if (this.rejected) {
|
||||
steps[steps.length - 1] = TITLES.rejected;
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
@computed get isValid () {
|
||||
const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError;
|
||||
const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.minBlockError && !this.totalError;
|
||||
const verifyValid = !this.passwordError;
|
||||
|
||||
switch (this.stage) {
|
||||
case 0:
|
||||
return detailsValid;
|
||||
|
||||
case 1:
|
||||
return this.extras
|
||||
? extrasValid
|
||||
: verifyValid;
|
||||
|
||||
case 2:
|
||||
return verifyValid;
|
||||
}
|
||||
}
|
||||
|
||||
get token () {
|
||||
return this.balance.tokens.find((balance) => balance.token.tag === this.tag).token;
|
||||
}
|
||||
|
||||
constructor (api, props) {
|
||||
this.api = api;
|
||||
|
||||
@ -135,6 +99,39 @@ export default class TransferStore {
|
||||
}
|
||||
}
|
||||
|
||||
@computed get steps () {
|
||||
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
|
||||
|
||||
if (this.rejected) {
|
||||
steps[steps.length - 1] = TITLES.rejected;
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
@computed get isValid () {
|
||||
const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError;
|
||||
const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.gasStore.conditionBlockError && !this.totalError;
|
||||
const verifyValid = !this.passwordError;
|
||||
|
||||
switch (this.stage) {
|
||||
case 0:
|
||||
return detailsValid;
|
||||
|
||||
case 1:
|
||||
return this.extras
|
||||
? extrasValid
|
||||
: verifyValid;
|
||||
|
||||
case 2:
|
||||
return verifyValid;
|
||||
}
|
||||
}
|
||||
|
||||
get token () {
|
||||
return this.balance.tokens.find((balance) => balance.token.tag === this.tag).token;
|
||||
}
|
||||
|
||||
@action onNext = () => {
|
||||
this.stage += 1;
|
||||
}
|
||||
@ -164,9 +161,6 @@ export default class TransferStore {
|
||||
case 'gasPrice':
|
||||
return this._onUpdateGasPrice(value);
|
||||
|
||||
case 'minBlock':
|
||||
return this._onUpdateMinBlock(value);
|
||||
|
||||
case 'recipient':
|
||||
return this._onUpdateRecipient(value);
|
||||
|
||||
@ -284,14 +278,6 @@ export default class TransferStore {
|
||||
this.recalculate();
|
||||
}
|
||||
|
||||
@action _onUpdateMinBlock = (minBlock) => {
|
||||
console.log('minBlock', minBlock);
|
||||
transaction(() => {
|
||||
this.minBlock = minBlock;
|
||||
this.minBlockError = this._validatePositiveNumber(minBlock);
|
||||
});
|
||||
}
|
||||
|
||||
@action _onUpdateGasPrice = (gasPrice) => {
|
||||
this.recalculate();
|
||||
}
|
||||
@ -590,7 +576,6 @@ export default class TransferStore {
|
||||
send () {
|
||||
const { options, values } = this._getTransferParams();
|
||||
|
||||
options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null;
|
||||
log.debug('@send', 'transfer value', options.value && options.value.toFormat());
|
||||
|
||||
return this._getTransferMethod().postTransaction(options, values);
|
||||
@ -639,15 +624,12 @@ export default class TransferStore {
|
||||
const to = (isEth && !isWallet) ? this.recipient
|
||||
: (this.isWallet ? this.wallet.address : this.token.address);
|
||||
|
||||
const options = {
|
||||
const options = this.gasStore.overrideTransaction({
|
||||
from: this.sender || this.account.address,
|
||||
to
|
||||
};
|
||||
});
|
||||
|
||||
if (!gas) {
|
||||
options.gas = this.gasStore.gas;
|
||||
options.gasPrice = this.gasStore.price;
|
||||
} else {
|
||||
if (gas) {
|
||||
options.gas = MAX_GAS_ESTIMATION;
|
||||
}
|
||||
|
||||
|
@ -207,7 +207,7 @@ class Transfer extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isEth, data, dataError, minBlock, minBlockError, total, totalError } = this.store;
|
||||
const { isEth, data, dataError, total, totalError } = this.store;
|
||||
|
||||
return (
|
||||
<Extras
|
||||
@ -215,8 +215,6 @@ class Transfer extends Component {
|
||||
dataError={ dataError }
|
||||
gasStore={ this.store.gasStore }
|
||||
isEth={ isEth }
|
||||
minBlock={ minBlock }
|
||||
minBlockError={ minBlockError }
|
||||
onChange={ this.store.onUpdateDetails }
|
||||
total={ total }
|
||||
totalError={ totalError }
|
||||
|
@ -52,7 +52,7 @@ export default class SignerMiddleware {
|
||||
}
|
||||
|
||||
onConfirmStart = (store, action) => {
|
||||
const { gas = 0, gasPrice = 0, id, password, payload, wallet } = action.payload;
|
||||
const { condition, gas = 0, gasPrice = 0, id, password, payload, wallet } = action.payload;
|
||||
|
||||
const handlePromise = (promise) => {
|
||||
promise
|
||||
@ -120,7 +120,7 @@ export default class SignerMiddleware {
|
||||
});
|
||||
}
|
||||
|
||||
handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice }, password));
|
||||
handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice, condition }, password));
|
||||
}
|
||||
|
||||
onRejectStart = (store, action) => {
|
||||
|
@ -78,6 +78,9 @@ class InputAddress extends Component {
|
||||
props.focused = focused;
|
||||
}
|
||||
|
||||
// FIXME: The is not advisable, fixes the display issue, however the name should come from
|
||||
// a common component.
|
||||
// account.name || (value ? 'UNNAMED' : value)
|
||||
return (
|
||||
<div className={ containerClasses.join(' ') }>
|
||||
<Input
|
||||
@ -96,7 +99,7 @@ class InputAddress extends Component {
|
||||
tabIndex={ tabIndex }
|
||||
value={
|
||||
text && account
|
||||
? account.name
|
||||
? (account.name || (value ? 'UNNAMED' : value))
|
||||
: (nullName || value)
|
||||
}
|
||||
{ ...props }
|
||||
|
17
js/src/ui/Form/InputDate/index.js
Normal file
17
js/src/ui/Form/InputDate/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright 2015-2017 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 './inputDate';
|
22
js/src/ui/Form/InputDate/inputDate.css
Normal file
22
js/src/ui/Form/InputDate/inputDate.css
Normal file
@ -0,0 +1,22 @@
|
||||
/* Copyright 2015-2017 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/>.
|
||||
*/
|
||||
|
||||
.container {
|
||||
.input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
53
js/src/ui/Form/InputDate/inputDate.js
Normal file
53
js/src/ui/Form/InputDate/inputDate.js
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright 2015-2017 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 { DatePicker } from 'material-ui';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import Label from '../Label';
|
||||
|
||||
import styles from './inputDate.css';
|
||||
|
||||
// NOTE: Has to be larger than Signer overlay Z, aligns with ../InputTime
|
||||
const DIALOG_STYLE = { zIndex: 10010 };
|
||||
|
||||
export default class InputDate extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
hint: PropTypes.node,
|
||||
label: PropTypes.node,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
render () {
|
||||
const { className, hint, label, onChange, value } = this.props;
|
||||
|
||||
return (
|
||||
<div className={ [styles.container, className].join(' ') }>
|
||||
<Label label={ label } />
|
||||
<DatePicker
|
||||
autoOk
|
||||
className={ styles.input }
|
||||
dialogContainerStyle={ DIALOG_STYLE }
|
||||
hintText={ hint }
|
||||
onChange={ onChange }
|
||||
value={ value }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
17
js/src/ui/Form/InputTime/index.js
Normal file
17
js/src/ui/Form/InputTime/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright 2015-2017 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 './inputTime';
|
22
js/src/ui/Form/InputTime/inputTime.css
Normal file
22
js/src/ui/Form/InputTime/inputTime.css
Normal file
@ -0,0 +1,22 @@
|
||||
/* Copyright 2015-2017 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/>.
|
||||
*/
|
||||
|
||||
.container {
|
||||
.input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
54
js/src/ui/Form/InputTime/inputTime.js
Normal file
54
js/src/ui/Form/InputTime/inputTime.js
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright 2015-2017 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 { TimePicker } from 'material-ui';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import Label from '../Label';
|
||||
|
||||
import styles from './inputTime.css';
|
||||
|
||||
// NOTE: Has to be larger than Signer overlay Z, aligns with ../InputDate
|
||||
const DIALOG_STYLE = { zIndex: 10010 };
|
||||
|
||||
export default class InputTime extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
hint: PropTypes.node,
|
||||
label: PropTypes.node,
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, hint, label, onChange, value } = this.props;
|
||||
|
||||
return (
|
||||
<div className={ [styles.container, className].join(' ') }>
|
||||
<Label label={ label } />
|
||||
<TimePicker
|
||||
autoOk
|
||||
className={ styles.input }
|
||||
dialogStyle={ DIALOG_STYLE }
|
||||
format='24hr'
|
||||
hintText={ hint }
|
||||
onChange={ onChange }
|
||||
value={ value }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
17
js/src/ui/Form/Label/index.js
Normal file
17
js/src/ui/Form/Label/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright 2015-2017 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 './label';
|
24
js/src/ui/Form/Label/label.css
Normal file
24
js/src/ui/Form/Label/label.css
Normal file
@ -0,0 +1,24 @@
|
||||
/* Copyright 2015-2017 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/>.
|
||||
*/
|
||||
|
||||
$labelColor: rgba(255, 255, 255, 0.5);
|
||||
$labelFontSize: 0.75rem;
|
||||
|
||||
.label {
|
||||
color: $labelColor;
|
||||
font-size: $labelFontSize;
|
||||
}
|
40
js/src/ui/Form/Label/label.js
Normal file
40
js/src/ui/Form/Label/label.js
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright 2015-2017 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 styles from './label.css';
|
||||
|
||||
export default class Label extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.node
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, label } = this.props;
|
||||
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<label className={ [styles.label, className].join(' ') }>
|
||||
{ label }
|
||||
</label>
|
||||
);
|
||||
}
|
||||
}
|
@ -15,18 +15,23 @@
|
||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.spaced {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
.container {
|
||||
.label {
|
||||
}
|
||||
|
||||
.typeContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.radioButton {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 0.5em;
|
||||
color: #ccc;
|
||||
z-index: 2;
|
||||
.radioLabel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.description {
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 0.5em;
|
||||
color: #ccc;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,10 +18,14 @@ import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import { arrayOrObjectProptype } from '~/util/proptypes';
|
||||
|
||||
import Label from '../Label';
|
||||
import styles from './radioButtons.css';
|
||||
|
||||
export default class RadioButtons extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.node,
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.any,
|
||||
@ -34,10 +38,10 @@ export default class RadioButtons extends Component {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, values } = this.props;
|
||||
const { className, label, value, values } = this.props;
|
||||
|
||||
const index = Number.isNaN(parseInt(value))
|
||||
? values.findIndex((val) => val.key === value)
|
||||
? values.findIndex((_value) => _value.key === value)
|
||||
: parseInt(value);
|
||||
const selectedValue = typeof value !== 'object'
|
||||
? values[index]
|
||||
@ -45,13 +49,19 @@ export default class RadioButtons extends Component {
|
||||
const key = this.getKey(selectedValue, index);
|
||||
|
||||
return (
|
||||
<RadioButtonGroup
|
||||
name={ name }
|
||||
onChange={ this.onChange }
|
||||
valueSelected={ key }
|
||||
>
|
||||
{ this.renderContent() }
|
||||
</RadioButtonGroup>
|
||||
<div className={ [styles.container, className].join(' ') }>
|
||||
<Label
|
||||
className={ styles.label }
|
||||
label={ label }
|
||||
/>
|
||||
<RadioButtonGroup
|
||||
name={ name }
|
||||
onChange={ this.onChange }
|
||||
valueSelected={ key }
|
||||
>
|
||||
{ this.renderContent() }
|
||||
</RadioButtonGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -67,14 +77,14 @@ export default class RadioButtons extends Component {
|
||||
|
||||
return (
|
||||
<RadioButton
|
||||
className={ styles.spaced }
|
||||
className={ styles.radioButton }
|
||||
key={ index }
|
||||
label={
|
||||
<div className={ styles.typeContainer }>
|
||||
<div className={ styles.radioLabel }>
|
||||
<span>{ label }</span>
|
||||
{
|
||||
description
|
||||
? <span className={ styles.desc }>{ description }</span>
|
||||
? <span className={ styles.description }>{ description }</span>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
@ -97,7 +107,7 @@ export default class RadioButtons extends Component {
|
||||
|
||||
onChange = (event, index) => {
|
||||
const { onChange, values } = this.props;
|
||||
const value = values[index] || values.find((v) => v.key === index);
|
||||
const value = values[index] || values.find((value) => value.key === index);
|
||||
|
||||
onChange(value, index);
|
||||
}
|
||||
|
@ -16,25 +16,31 @@
|
||||
|
||||
import AddressSelect from './AddressSelect';
|
||||
import FormWrap from './FormWrap';
|
||||
import TypedInput from './TypedInput';
|
||||
import Input from './Input';
|
||||
import InputAddress from './InputAddress';
|
||||
import InputAddressSelect from './InputAddressSelect';
|
||||
import InputChip from './InputChip';
|
||||
import InputDate from './InputDate';
|
||||
import InputInline from './InputInline';
|
||||
import Select from './Select';
|
||||
import InputTime from './InputTime';
|
||||
import Label from './Label';
|
||||
import RadioButtons from './RadioButtons';
|
||||
import Select from './Select';
|
||||
import TypedInput from './TypedInput';
|
||||
|
||||
export default from './form';
|
||||
export {
|
||||
AddressSelect,
|
||||
FormWrap,
|
||||
TypedInput,
|
||||
Input,
|
||||
InputAddress,
|
||||
InputAddressSelect,
|
||||
InputChip,
|
||||
InputDate,
|
||||
InputInline,
|
||||
InputTime,
|
||||
Label,
|
||||
RadioButtons,
|
||||
Select,
|
||||
RadioButtons
|
||||
TypedInput
|
||||
};
|
||||
|
@ -16,6 +16,46 @@
|
||||
*/
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.conditionContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
margin-bottom: 1.5em;
|
||||
|
||||
.input {
|
||||
flex: 0 1 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.conditionRadio {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1em;
|
||||
|
||||
&>label {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
&>div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
&>div {
|
||||
width: auto !important;
|
||||
|
||||
label {
|
||||
padding-right: 1.5em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.graphContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
|
@ -17,13 +17,44 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { observer } from 'mobx-react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Input from '../Form/Input';
|
||||
import { Input, InputDate, InputTime, RadioButtons } from '../Form';
|
||||
import GasPriceSelector from '../GasPriceSelector';
|
||||
import Store from './store';
|
||||
|
||||
import Store, { CONDITIONS } from './store';
|
||||
import styles from './gasPriceEditor.css';
|
||||
|
||||
const CONDITION_VALUES = [
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='txEditor.condition.none'
|
||||
defaultMessage='No conditions'
|
||||
/>
|
||||
),
|
||||
key: CONDITIONS.NONE
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='txEditor.condition.blocknumber'
|
||||
defaultMessage='Send after BlockNumber'
|
||||
/>
|
||||
),
|
||||
key: CONDITIONS.BLOCK
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='txEditor.condition.datetime'
|
||||
defaultMessage='Send after Date & Time'
|
||||
/>
|
||||
),
|
||||
key: CONDITIONS.TIME
|
||||
}
|
||||
];
|
||||
|
||||
@observer
|
||||
export default class GasPriceEditor extends Component {
|
||||
static contextTypes = {
|
||||
@ -41,7 +72,7 @@ export default class GasPriceEditor extends Component {
|
||||
render () {
|
||||
const { api } = this.context;
|
||||
const { children, store } = this.props;
|
||||
const { errorGas, errorPrice, errorTotal, estimated, gas, histogram, price, priceDefault, totalValue } = store;
|
||||
const { conditionType, errorGas, errorPrice, errorTotal, estimated, gas, histogram, price, priceDefault, totalValue } = store;
|
||||
|
||||
const eth = api.util.fromWei(totalValue).toFormat();
|
||||
const gasLabel = `gas (estimated: ${new BigNumber(estimated).toFormat()})`;
|
||||
@ -49,46 +80,146 @@ export default class GasPriceEditor extends Component {
|
||||
|
||||
return (
|
||||
<div className={ styles.container }>
|
||||
<div className={ styles.graphColumn }>
|
||||
<GasPriceSelector
|
||||
histogram={ histogram }
|
||||
onChange={ this.onEditGasPrice }
|
||||
price={ price }
|
||||
/>
|
||||
<div className={ styles.gasPriceDesc }>
|
||||
You can choose the gas price based on the distribution of recent included transaction gas prices. The lower the gas price is, the cheaper the transaction will be. The higher the gas price is, the faster it should get mined by the network.
|
||||
<RadioButtons
|
||||
className={ styles.conditionRadio }
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='txEditor.condition.label'
|
||||
defaultMessage='Condition where transaction activates'
|
||||
/>
|
||||
}
|
||||
onChange={ this.onChangeConditionType }
|
||||
value={ conditionType }
|
||||
values={ CONDITION_VALUES }
|
||||
/>
|
||||
{ this.renderConditions() }
|
||||
|
||||
<div className={ styles.graphContainer }>
|
||||
<div className={ styles.graphColumn }>
|
||||
<GasPriceSelector
|
||||
histogram={ histogram }
|
||||
onChange={ this.onEditGasPrice }
|
||||
price={ price }
|
||||
/>
|
||||
<div className={ styles.gasPriceDesc }>
|
||||
<FormattedMessage
|
||||
id='txEditor.gas.info'
|
||||
defaultMessage='You can choose the gas price based on the distribution of recent included transaction gas prices. The lower the gas price is, the cheaper the transaction will be. The higher the gas price is, the faster it should get mined by the network.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={ styles.editColumn }>
|
||||
<div className={ styles.row }>
|
||||
<Input
|
||||
error={ errorGas }
|
||||
hint='the amount of gas to use for the transaction'
|
||||
label={ gasLabel }
|
||||
min={ 1 }
|
||||
onChange={ this.onEditGas }
|
||||
type='number'
|
||||
value={ gas }
|
||||
/>
|
||||
<Input
|
||||
error={ errorPrice }
|
||||
hint='the price of gas to use for the transaction'
|
||||
label={ priceLabel }
|
||||
min={ 1 }
|
||||
onChange={ this.onEditGasPrice }
|
||||
type='number'
|
||||
value={ price }
|
||||
/>
|
||||
</div>
|
||||
<div className={ styles.row }>
|
||||
<Input
|
||||
disabled
|
||||
error={ errorTotal }
|
||||
hint='the total amount of the transaction'
|
||||
label='total transaction amount'
|
||||
value={ `${eth} ETH` }
|
||||
/>
|
||||
</div>
|
||||
<div className={ styles.row }>
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div className={ styles.editColumn }>
|
||||
<div className={ styles.row }>
|
||||
renderConditions () {
|
||||
const { conditionType, condition, conditionBlockError } = this.props.store;
|
||||
|
||||
if (conditionType === CONDITIONS.NONE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (conditionType === CONDITIONS.BLOCK) {
|
||||
return (
|
||||
<div className={ styles.conditionContainer }>
|
||||
<div className={ styles.input }>
|
||||
<Input
|
||||
error={ errorGas }
|
||||
hint='the amount of gas to use for the transaction'
|
||||
label={ gasLabel }
|
||||
onChange={ this.onEditGas }
|
||||
value={ gas }
|
||||
/>
|
||||
<Input
|
||||
error={ errorPrice }
|
||||
hint='the price of gas to use for the transaction'
|
||||
label={ priceLabel }
|
||||
onChange={ this.onEditGasPrice }
|
||||
value={ price }
|
||||
error={ conditionBlockError }
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='txEditor.condition.block.hint'
|
||||
defaultMessage='The minimum block to send from'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='txEditor.condition.block.label'
|
||||
defaultMessage='Transaction send block'
|
||||
/>
|
||||
}
|
||||
min={ 1 }
|
||||
onChange={ this.onChangeConditionBlock }
|
||||
type='number'
|
||||
value={ condition.block }
|
||||
/>
|
||||
</div>
|
||||
<div className={ styles.row }>
|
||||
<Input
|
||||
disabled
|
||||
error={ errorTotal }
|
||||
hint='the total amount of the transaction'
|
||||
label='total transaction amount'
|
||||
value={ `${eth} ETH` }
|
||||
/>
|
||||
</div>
|
||||
<div className={ styles.row }>
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.conditionContainer }>
|
||||
<div className={ styles.input }>
|
||||
<InputDate
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='txEditor.condition.date.hint'
|
||||
defaultMessage='The minimum date to send from'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='txEditor.condition.date.label'
|
||||
defaultMessage='Transaction send date'
|
||||
/>
|
||||
}
|
||||
onChange={ this.onChangeConditionDateTime }
|
||||
value={ condition.time }
|
||||
/>
|
||||
</div>
|
||||
<div className={ styles.input }>
|
||||
<InputTime
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='txEditor.condition.time.hint'
|
||||
defaultMessage='The minimum time to send from'
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='txEditor.condition.time.label'
|
||||
defaultMessage='Transaction send time'
|
||||
/>
|
||||
}
|
||||
onChange={ this.onChangeConditionDateTime }
|
||||
value={ condition.time }
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -107,4 +238,16 @@ export default class GasPriceEditor extends Component {
|
||||
store.setPrice(price);
|
||||
onChange && onChange('gasPrice', price);
|
||||
}
|
||||
|
||||
onChangeConditionType = (conditionType) => {
|
||||
this.props.store.setConditionType(conditionType.key);
|
||||
}
|
||||
|
||||
onChangeConditionBlock = (event, blockNumber) => {
|
||||
this.props.store.setConditionBlockNumber(blockNumber);
|
||||
}
|
||||
|
||||
onChangeConditionDateTime = (event, datetime) => {
|
||||
this.props.store.setConditionDateTime(datetime);
|
||||
}
|
||||
}
|
||||
|
@ -21,26 +21,64 @@ import sinon from 'sinon';
|
||||
|
||||
import GasPriceEditor from './';
|
||||
|
||||
const api = {
|
||||
util: {
|
||||
fromWei: (value) => new BigNumber(value)
|
||||
}
|
||||
};
|
||||
let api;
|
||||
let component;
|
||||
let store;
|
||||
|
||||
const store = {
|
||||
estimated: '123',
|
||||
histogram: {},
|
||||
priceDefault: '456',
|
||||
totalValue: '789',
|
||||
setGas: sinon.stub(),
|
||||
setPrice: sinon.stub()
|
||||
};
|
||||
function createApi () {
|
||||
api = {
|
||||
eth: {
|
||||
blockNumber: sinon.stub().resolves(new BigNumber(3))
|
||||
},
|
||||
util: {
|
||||
fromWei: (value) => new BigNumber(value)
|
||||
}
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
function createStore () {
|
||||
createApi();
|
||||
|
||||
store = {
|
||||
_api: api,
|
||||
conditionType: 'none',
|
||||
estimated: '123',
|
||||
histogram: {},
|
||||
priceDefault: '456',
|
||||
totalValue: '789',
|
||||
setGas: sinon.stub(),
|
||||
setPrice: sinon.stub()
|
||||
};
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
function render (props = {}) {
|
||||
createStore();
|
||||
|
||||
component = shallow(
|
||||
<GasPriceEditor
|
||||
store={ store }
|
||||
{ ...props }
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
api
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
describe('ui/GasPriceEditor', () => {
|
||||
beforeEach(() => {
|
||||
render();
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(shallow(
|
||||
<GasPriceEditor store={ store } />,
|
||||
{ context: { api } }
|
||||
)).to.be.ok;
|
||||
expect(component).to.be.ok;
|
||||
});
|
||||
});
|
||||
|
@ -20,7 +20,17 @@ import { action, computed, observable, transaction } from 'mobx';
|
||||
import { ERRORS, validatePositiveNumber } from '~/util/validation';
|
||||
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants';
|
||||
|
||||
const CONDITIONS = {
|
||||
NONE: 'none',
|
||||
BLOCK: 'blockNumber',
|
||||
TIME: 'timestamp'
|
||||
};
|
||||
|
||||
export default class GasPriceEditor {
|
||||
@observable blockNumber = 0;
|
||||
@observable condition = {};
|
||||
@observable conditionBlockError = null;
|
||||
@observable conditionType = CONDITIONS.NONE;
|
||||
@observable errorEstimated = null;
|
||||
@observable errorGas = null;
|
||||
@observable errorPrice = null;
|
||||
@ -34,13 +44,23 @@ export default class GasPriceEditor {
|
||||
@observable priceDefault;
|
||||
@observable weiValue = '0';
|
||||
|
||||
constructor (api, { gas, gasLimit, gasPrice }) {
|
||||
constructor (api, { gas, gasLimit, gasPrice, condition = null }) {
|
||||
this._api = api;
|
||||
|
||||
this.gas = gas;
|
||||
this.gasLimit = gasLimit;
|
||||
this.price = gasPrice;
|
||||
|
||||
if (condition) {
|
||||
if (condition.block) {
|
||||
this.condition = { block: condition.block.toFixed(0) };
|
||||
this.conditionType = CONDITIONS.BLOCK;
|
||||
} else if (condition.time) {
|
||||
this.condition = { time: condition.time };
|
||||
this.conditionType = CONDITIONS.TIME;
|
||||
}
|
||||
}
|
||||
|
||||
if (api) {
|
||||
this.loadDefaults();
|
||||
}
|
||||
@ -54,6 +74,39 @@ export default class GasPriceEditor {
|
||||
}
|
||||
}
|
||||
|
||||
@action setConditionType = (conditionType = CONDITIONS.NONE) => {
|
||||
transaction(() => {
|
||||
this.conditionBlockError = null;
|
||||
this.conditionType = conditionType;
|
||||
|
||||
switch (conditionType) {
|
||||
case CONDITIONS.BLOCK:
|
||||
this.condition = Object.assign({}, this.condition, { block: this.blockNumber || 1 });
|
||||
break;
|
||||
|
||||
case CONDITIONS.TIME:
|
||||
this.condition = Object.assign({}, this.condition, { time: new Date() });
|
||||
break;
|
||||
|
||||
case CONDITIONS.NONE:
|
||||
default:
|
||||
this.condition = {};
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@action setConditionBlockNumber = (block) => {
|
||||
transaction(() => {
|
||||
this.conditionBlockError = validatePositiveNumber(block).numberError;
|
||||
this.condition = Object.assign({}, this.condition, { block });
|
||||
});
|
||||
}
|
||||
|
||||
@action setConditionDateTime = (time) => {
|
||||
this.condition = Object.assign({}, this.condition, { time });
|
||||
}
|
||||
|
||||
@action setEditing = (isEditing) => {
|
||||
this.isEditing = isEditing;
|
||||
}
|
||||
@ -130,9 +183,10 @@ export default class GasPriceEditor {
|
||||
bucket_bounds: [],
|
||||
counts: []
|
||||
})),
|
||||
this._api.eth.gasPrice()
|
||||
this._api.eth.gasPrice(),
|
||||
this._api.eth.blockNumber()
|
||||
])
|
||||
.then(([histogram, _price]) => {
|
||||
.then(([histogram, _price, blockNumber]) => {
|
||||
transaction(() => {
|
||||
const price = _price.toFixed(0);
|
||||
|
||||
@ -142,6 +196,7 @@ export default class GasPriceEditor {
|
||||
this.setHistogram(histogram);
|
||||
|
||||
this.priceDefault = price;
|
||||
this.blockNumber = blockNumber.toNumber();
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -150,13 +205,37 @@ export default class GasPriceEditor {
|
||||
}
|
||||
|
||||
overrideTransaction = (transaction) => {
|
||||
if (this.errorGas || this.errorPrice) {
|
||||
if (this.errorGas || this.errorPrice || this.conditionBlockError) {
|
||||
return transaction;
|
||||
}
|
||||
|
||||
return Object.assign({}, transaction, {
|
||||
const override = {
|
||||
condition: this.condition,
|
||||
gas: new BigNumber(this.gas || DEFAULT_GAS),
|
||||
gasPrice: new BigNumber(this.price || DEFAULT_GASPRICE)
|
||||
});
|
||||
};
|
||||
|
||||
const result = Object.assign({}, transaction, override);
|
||||
|
||||
switch (this.conditionType) {
|
||||
case CONDITIONS.BLOCK:
|
||||
result.condition = { block: new BigNumber(this.condition.block || 0) };
|
||||
break;
|
||||
|
||||
case CONDITIONS.TIME:
|
||||
result.condition = { time: this.condition.time };
|
||||
break;
|
||||
|
||||
case CONDITIONS.NONE:
|
||||
default:
|
||||
delete result.condition;
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
CONDITIONS
|
||||
};
|
||||
|
@ -21,6 +21,7 @@ import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/consta
|
||||
import { ERRORS } from '~/util/validation';
|
||||
|
||||
import GasPriceEditor from './gasPriceEditor';
|
||||
import { CONDITIONS } from './store';
|
||||
|
||||
const { Store } = GasPriceEditor;
|
||||
|
||||
@ -31,18 +32,30 @@ const HISTOGRAM = {
|
||||
counts: [3, 4]
|
||||
};
|
||||
|
||||
const api = {
|
||||
eth: {
|
||||
gasPrice: sinon.stub().resolves(GASPRICE)
|
||||
},
|
||||
parity: {
|
||||
gasPriceHistogram: sinon.stub().resolves(HISTOGRAM)
|
||||
}
|
||||
};
|
||||
let api;
|
||||
|
||||
describe('ui/GasPriceEditor/store', () => {
|
||||
// TODO: share with gasPriceEditor.spec.js
|
||||
function createApi () {
|
||||
api = {
|
||||
eth: {
|
||||
blockNumber: sinon.stub().resolves(new BigNumber(2)),
|
||||
gasPrice: sinon.stub().resolves(GASPRICE)
|
||||
},
|
||||
parity: {
|
||||
gasPriceHistogram: sinon.stub().resolves(HISTOGRAM)
|
||||
}
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
describe('ui/GasPriceEditor/Store', () => {
|
||||
let store = null;
|
||||
|
||||
beforeEach(() => {
|
||||
createApi();
|
||||
});
|
||||
|
||||
it('is available via GasPriceEditor.Store', () => {
|
||||
expect(new Store(null, {})).to.be.ok;
|
||||
});
|
||||
@ -65,6 +78,7 @@ describe('ui/GasPriceEditor/store', () => {
|
||||
describe('constructor (defaults) when histogram not available', () => {
|
||||
const api = {
|
||||
eth: {
|
||||
blockNumber: sinon.stub().resolves(new BigNumber(2)),
|
||||
gasPrice: sinon.stub().resolves(GASPRICE)
|
||||
},
|
||||
parity: {
|
||||
@ -92,6 +106,67 @@ describe('ui/GasPriceEditor/store', () => {
|
||||
store = new Store(null, { gasLimit: GASLIMIT });
|
||||
});
|
||||
|
||||
describe('setConditionType', () => {
|
||||
it('sets the actual type', () => {
|
||||
store.setConditionType('testingType');
|
||||
expect(store.conditionType).to.equal('testingType');
|
||||
});
|
||||
|
||||
it('clears any block error on changing type', () => {
|
||||
store.setConditionBlockNumber(-1);
|
||||
expect(store.conditionBlockError).not.to.be.null;
|
||||
store.setConditionType(CONDITIONS.BLOCK);
|
||||
expect(store.conditionBlockError).to.be.null;
|
||||
});
|
||||
|
||||
it('sets condition.block when type === CONDITIONS.BLOCK', () => {
|
||||
store.setConditionType(CONDITIONS.BLOCK);
|
||||
expect(store.condition.block).to.be.ok;
|
||||
});
|
||||
|
||||
it('clears condition when type === CONDITIONS.NONE', () => {
|
||||
store.setConditionType(CONDITIONS.BLOCK);
|
||||
store.setConditionType(CONDITIONS.NONE);
|
||||
expect(store.condition).to.deep.equal({});
|
||||
});
|
||||
|
||||
it('sets condition.time when type === CONDITIONS.TIME', () => {
|
||||
store.setConditionType(CONDITIONS.TIME);
|
||||
expect(store.condition.time).to.be.ok;
|
||||
});
|
||||
});
|
||||
|
||||
describe('setConditionBlockNumber', () => {
|
||||
beforeEach(() => {
|
||||
store.setConditionBlockNumber('testingBlock');
|
||||
});
|
||||
|
||||
it('sets the blockNumber', () => {
|
||||
expect(store.condition.block).to.equal('testingBlock');
|
||||
});
|
||||
|
||||
it('sets the error on invalid numbers', () => {
|
||||
expect(store.conditionBlockError).not.to.be.null;
|
||||
});
|
||||
|
||||
it('sets the error on negative numbers', () => {
|
||||
store.setConditionBlockNumber(-1);
|
||||
expect(store.conditionBlockError).not.to.be.null;
|
||||
});
|
||||
|
||||
it('clears the error on positive numbers', () => {
|
||||
store.setConditionBlockNumber(1000);
|
||||
expect(store.conditionBlockError).to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
describe('setConditionDateTime', () => {
|
||||
it('sets the datatime', () => {
|
||||
store.setConditionDateTime('testingDateTime');
|
||||
expect(store.condition.time).to.equal('testingDateTime');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setEditing', () => {
|
||||
it('sets the value', () => {
|
||||
expect(store.isEditing).to.be.false;
|
||||
|
@ -14,9 +14,10 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { CircularProgress } from 'material-ui';
|
||||
import moment from 'moment';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import CircularProgress from 'material-ui/CircularProgress';
|
||||
|
||||
import { TypedInput, InputAddress } from '../Form';
|
||||
import MethodDecodingStore from './methodDecodingStore';
|
||||
@ -128,15 +129,25 @@ class MethodDecoding extends Component {
|
||||
|
||||
renderMinBlock () {
|
||||
const { historic, transaction } = this.props;
|
||||
const { minBlock } = transaction;
|
||||
const { condition } = transaction;
|
||||
|
||||
if (!minBlock || minBlock.eq(0)) {
|
||||
if (!condition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span>, { historic ? 'Submitted' : 'Submission' } at block <span className={ styles.highlight }>#{ minBlock.toFormat(0) }</span></span>
|
||||
);
|
||||
if (condition.block && condition.block.gt(0)) {
|
||||
return (
|
||||
<span>, { historic ? 'Submitted' : 'Submission' } at block <span className={ styles.highlight }>#{ condition.block.toFormat(0) }</span></span>
|
||||
);
|
||||
}
|
||||
|
||||
if (condition.time) {
|
||||
return (
|
||||
<span>, { historic ? 'Submitted' : 'Submission' } at <span className={ styles.highlight }>{ moment(condition.time).format('LLLL') }</span></span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderAction () {
|
||||
|
@ -35,7 +35,7 @@ import DappIcon from './DappIcon';
|
||||
import Editor from './Editor';
|
||||
import Errors from './Errors';
|
||||
import Features, { FEATURES, FeaturesStore } from './Features';
|
||||
import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form';
|
||||
import Form, { AddressSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form';
|
||||
import GasPriceEditor from './GasPriceEditor';
|
||||
import GasPriceSelector from './GasPriceSelector';
|
||||
import Icons from './Icons';
|
||||
@ -95,9 +95,12 @@ export {
|
||||
InputAddress,
|
||||
InputAddressSelect,
|
||||
InputChip,
|
||||
InputDate,
|
||||
InputInline,
|
||||
InputTime,
|
||||
IdentityIcon,
|
||||
IdentityName,
|
||||
Label,
|
||||
LanguageSelector,
|
||||
Loading,
|
||||
MethodDecoding,
|
||||
|
@ -15,6 +15,9 @@
|
||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
$overlayZ: 10000;
|
||||
$modalZ: 10001;
|
||||
|
||||
.account {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@ -61,7 +64,7 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
z-index: 10000;
|
||||
z-index: $overlayZ;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@ -70,7 +73,7 @@
|
||||
position: fixed;
|
||||
font-size: 16px;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
z-index: 10001;
|
||||
z-index: $modalZ;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@ -109,7 +112,8 @@
|
||||
border-radius: 4px 4px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 50vh;
|
||||
min-height: 30vh;
|
||||
max-height: 80vh;
|
||||
max-width: calc(100vw - 1em);
|
||||
|
||||
.content {
|
||||
|
@ -73,14 +73,14 @@ export default class TransactionMainDetails extends Component {
|
||||
: transaction
|
||||
}
|
||||
/>
|
||||
{ this.renderEditGas() }
|
||||
{ this.renderEditTx() }
|
||||
</div>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderEditGas () {
|
||||
renderEditTx () {
|
||||
const { gasStore } = this.props;
|
||||
|
||||
if (!gasStore) {
|
||||
@ -91,7 +91,7 @@ export default class TransactionMainDetails extends Component {
|
||||
<div className={ styles.editButtonRow }>
|
||||
<Button
|
||||
icon={ <MapsLocalGasStation /> }
|
||||
label='Edit gas/gasPrice'
|
||||
label='Edit conditions/gas/gasPrice'
|
||||
onClick={ this.toggleGasEditor }
|
||||
/>
|
||||
</div>
|
||||
|
@ -45,6 +45,7 @@ export default class TransactionPending extends Component {
|
||||
onReject: PropTypes.func.isRequired,
|
||||
store: PropTypes.object.isRequired,
|
||||
transaction: PropTypes.shape({
|
||||
condition: PropTypes.object,
|
||||
data: PropTypes.string,
|
||||
from: PropTypes.string.isRequired,
|
||||
gas: PropTypes.object.isRequired,
|
||||
@ -59,6 +60,7 @@ export default class TransactionPending extends Component {
|
||||
};
|
||||
|
||||
gasStore = new GasPriceEditor.Store(this.context.api, {
|
||||
condition: this.props.transaction.condition,
|
||||
gas: this.props.transaction.gas.toFixed(),
|
||||
gasLimit: this.props.gasLimit,
|
||||
gasPrice: this.props.transaction.gasPrice.toFixed()
|
||||
@ -80,7 +82,7 @@ export default class TransactionPending extends Component {
|
||||
|
||||
render () {
|
||||
return this.gasStore.isEditing
|
||||
? this.renderGasEditor()
|
||||
? this.renderTxEditor()
|
||||
: this.renderTransaction();
|
||||
}
|
||||
|
||||
@ -115,7 +117,7 @@ export default class TransactionPending extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderGasEditor () {
|
||||
renderTxEditor () {
|
||||
const { className } = this.props;
|
||||
|
||||
return (
|
||||
@ -133,15 +135,21 @@ export default class TransactionPending extends Component {
|
||||
onConfirm = (data) => {
|
||||
const { id, transaction } = this.props;
|
||||
const { password, wallet } = data;
|
||||
const { gas, gasPrice } = this.gasStore.overrideTransaction(transaction);
|
||||
const { condition, gas, gasPrice } = this.gasStore.overrideTransaction(transaction);
|
||||
|
||||
this.props.onConfirm({
|
||||
const options = {
|
||||
gas,
|
||||
gasPrice,
|
||||
id,
|
||||
password,
|
||||
wallet
|
||||
});
|
||||
};
|
||||
|
||||
if (condition && (condition.block || condition.time)) {
|
||||
options.condition = condition;
|
||||
}
|
||||
|
||||
this.props.onConfirm(options);
|
||||
}
|
||||
|
||||
onReject = () => {
|
||||
|
Loading…
Reference in New Issue
Block a user