Edit ETH value, gas and gas price in Contract Deployment (#4919)

* Fix typo

* Add Value capabilities to Contract Deployment

* Add Extras settings for Contract Deployment (#4483)

* Fix deploy in API
This commit is contained in:
Nicolas Gotchac 2017-03-16 13:18:28 +01:00 committed by Gav Wood
parent 57d718fde1
commit 7846544c1b
9 changed files with 300 additions and 73 deletions

View File

@ -107,13 +107,25 @@ export default class Contract {
}); });
} }
deploy (options, values, statecb = () => {}) { deploy (options, values, statecb = () => {}, skipGasEstimate = false) {
statecb(null, { state: 'estimateGas' }); let gasEstPromise;
return this if (skipGasEstimate) {
.deployEstimateGas(options, values) gasEstPromise = Promise.resolve(null);
.then(([gasEst, gas]) => { } else {
options.gas = gas.toFixed(0); statecb(null, { state: 'estimateGas' });
gasEstPromise = this.deployEstimateGas(options, values)
.then(([gasEst, gas]) => gas);
}
return gasEstPromise
.then((_gas) => {
if (_gas) {
options.gas = _gas.toFixed(0);
}
const gas = _gas || options.gas;
statecb(null, { state: 'postTransaction', gas }); statecb(null, { state: 'postTransaction', gas });

View File

@ -16,12 +16,16 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { MenuItem } from 'material-ui'; import { Checkbox, MenuItem } from 'material-ui';
import { AddressSelect, Form, Input, Select } from '~/ui'; import { AddressSelect, Form, Input, Select } from '~/ui';
import { validateAbi } from '~/util/validation'; import { validateAbi } from '~/util/validation';
import { parseAbiType } from '~/util/abi'; import { parseAbiType } from '~/util/abi';
const CHECK_STYLE = {
marginTop: '1em'
};
export default class DetailsStep extends Component { export default class DetailsStep extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object.isRequired api: PropTypes.object.isRequired
@ -30,8 +34,10 @@ export default class DetailsStep extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
onAbiChange: PropTypes.func.isRequired, onAbiChange: PropTypes.func.isRequired,
onAmountChange: PropTypes.func.isRequired,
onCodeChange: PropTypes.func.isRequired, onCodeChange: PropTypes.func.isRequired,
onDescriptionChange: PropTypes.func.isRequired, onDescriptionChange: PropTypes.func.isRequired,
onExtrasChange: PropTypes.func.isRequired,
onFromAddressChange: PropTypes.func.isRequired, onFromAddressChange: PropTypes.func.isRequired,
onInputsChange: PropTypes.func.isRequired, onInputsChange: PropTypes.func.isRequired,
onNameChange: PropTypes.func.isRequired, onNameChange: PropTypes.func.isRequired,
@ -39,11 +45,14 @@ export default class DetailsStep extends Component {
abi: PropTypes.string, abi: PropTypes.string,
abiError: PropTypes.string, abiError: PropTypes.string,
amount: PropTypes.string,
amountError: PropTypes.string,
balances: PropTypes.object, balances: PropTypes.object,
code: PropTypes.string, code: PropTypes.string,
codeError: PropTypes.string, codeError: PropTypes.string,
description: PropTypes.string, description: PropTypes.string,
descriptionError: PropTypes.string, descriptionError: PropTypes.string,
extras: PropTypes.bool,
fromAddress: PropTypes.string, fromAddress: PropTypes.string,
fromAddressError: PropTypes.string, fromAddressError: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
@ -52,6 +61,7 @@ export default class DetailsStep extends Component {
}; };
static defaultProps = { static defaultProps = {
extras: false,
readOnly: false readOnly: false
}; };
@ -83,7 +93,7 @@ export default class DetailsStep extends Component {
fromAddress, fromAddressError, fromAddress, fromAddressError,
name, nameError, name, nameError,
description, descriptionError, description, descriptionError,
abiError, abiError, extras,
code, codeError code, codeError
} = this.props; } = this.props;
@ -189,10 +199,70 @@ export default class DetailsStep extends Component {
value={ code } value={ code }
/> />
{ this.renderValueInput() }
<div>
<Checkbox
checked={ extras }
label={
<FormattedMessage
id='deployContract.details.advanced.label'
defaultMessage='advanced sending options'
/>
}
onCheck={ this.onCheckExtras }
style={ CHECK_STYLE }
/>
</div>
</Form> </Form>
); );
} }
renderValueInput () {
const { abi, amount, amountError } = this.props;
let payable = false;
try {
const parsedAbi = JSON.parse(abi);
payable = parsedAbi.find((method) => method.type === 'constructor' && method.payable);
} catch (error) {
return null;
}
if (!payable) {
return null;
}
return (
<Input
error={ amountError }
hint={
<FormattedMessage
id='deployContract.details.amount.hint'
defaultMessage='the amount to transfer to the contract'
/>
}
label={
<FormattedMessage
id='deployContract.details.amount.label'
defaultMessage='amount to transfer (in {tag})'
values={ {
tag: 'ETH'
} }
/>
}
min={ 0 }
step={ 0.1 }
type='number'
onChange={ this.onAmountChange }
value={ amount }
/>
);
}
renderContractSelect () { renderContractSelect () {
const { contracts } = this.state; const { contracts } = this.state;
@ -295,6 +365,16 @@ export default class DetailsStep extends Component {
onDescriptionChange(description); onDescriptionChange(description);
} }
onAmountChange = (event, value) => {
const { onAmountChange } = this.props;
onAmountChange(value);
}
onCheckExtras = () => {
this.props.onExtrasChange(!this.props.extras);
}
onAbiChange = (abi) => { onAbiChange = (abi) => {
const { api } = this.context; const { api } = this.context;
const { onAbiChange, onParamsChange, onInputsChange } = this.props; const { onAbiChange, onParamsChange, onInputsChange } = this.props;

View File

@ -14,6 +14,7 @@
// 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 { pick } from 'lodash';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
@ -22,12 +23,13 @@ import { connect } from 'react-redux';
import { BusyStep, Button, CompletedStep, CopyToClipboard, GasPriceEditor, IdentityIcon, Portal, TxHash, Warning } from '~/ui'; import { BusyStep, Button, CompletedStep, CopyToClipboard, GasPriceEditor, IdentityIcon, Portal, TxHash, Warning } from '~/ui';
import { CancelIcon, DoneIcon } from '~/ui/Icons'; import { CancelIcon, DoneIcon } from '~/ui/Icons';
import { ERRORS, validateAbi, validateCode, validateName } from '~/util/validation'; import { ERRORS, validateAbi, validateCode, validateName, validatePositiveNumber } from '~/util/validation';
import { deploy, deployEstimateGas } from '~/util/tx'; import { deploy, deployEstimateGas } from '~/util/tx';
import DetailsStep from './DetailsStep'; import DetailsStep from './DetailsStep';
import ParametersStep from './ParametersStep'; import ParametersStep from './ParametersStep';
import ErrorStep from './ErrorStep'; import ErrorStep from './ErrorStep';
import Extras from '../Transfer/Extras';
import styles from './deployContract.css'; import styles from './deployContract.css';
@ -50,6 +52,14 @@ const STEPS = {
/> />
) )
}, },
EXTRAS: {
title: (
<FormattedMessage
id='deployContract.title.extras'
defaultMessage='extra information'
/>
)
},
DEPLOYMENT: { DEPLOYMENT: {
waiting: true, waiting: true,
title: ( title: (
@ -97,12 +107,16 @@ class DeployContract extends Component {
state = { state = {
abi: '', abi: '',
abiError: ERRORS.invalidAbi, abiError: ERRORS.invalidAbi,
amount: '0',
amountValue: new BigNumber(0),
amountError: '',
code: '', code: '',
codeError: ERRORS.invalidCode, codeError: ERRORS.invalidCode,
deployState: '', deployState: '',
deployError: null, deployError: null,
description: '', description: '',
descriptionError: null, descriptionError: null,
extras: false,
fromAddress: Object.keys(this.props.accounts)[0], fromAddress: Object.keys(this.props.accounts)[0],
fromAddressError: null, fromAddressError: null,
name: '', name: '',
@ -144,7 +158,19 @@ class DeployContract extends Component {
const realStepKeys = deployError || rejected const realStepKeys = deployError || rejected
? [] ? []
: Object.keys(STEPS).filter((k) => k !== 'CONTRACT_PARAMETERS' || inputs.length > 0); : Object.keys(STEPS)
.filter((k) => {
if (k === 'CONTRACT_PARAMETERS') {
return inputs.length > 0;
}
if (k === 'EXTRAS') {
return this.state.extras;
}
return true;
});
const realStep = realStepKeys.findIndex((k) => k === step); const realStep = realStepKeys.findIndex((k) => k === step);
const realSteps = realStepKeys.length const realSteps = realStepKeys.length
? realStepKeys.map((k) => STEPS[k]) ? realStepKeys.map((k) => STEPS[k])
@ -207,8 +233,8 @@ class DeployContract extends Component {
} }
renderDialogActions () { renderDialogActions () {
const { deployError, abiError, codeError, nameError, descriptionError, fromAddressError, fromAddress, step } = this.state; const { deployError, abiError, amountError, codeError, nameError, descriptionError, fromAddressError, fromAddress, step } = this.state;
const isValid = !nameError && !fromAddressError && !descriptionError && !abiError && !codeError; const isValid = !nameError && !fromAddressError && !descriptionError && !abiError && !codeError && !amountError;
const cancelBtn = ( const cancelBtn = (
<Button <Button
@ -256,48 +282,69 @@ class DeployContract extends Component {
return closeBtn; return closeBtn;
} }
const createButton = (
<Button
icon={
<IdentityIcon
address={ fromAddress }
button
/>
}
key='create'
label={
<FormattedMessage
id='deployContract.button.create'
defaultMessage='Create'
/>
}
onClick={ this.onDeployStart }
/>
);
const nextButton = (
<Button
disabled={ !isValid }
key='next'
icon={
<IdentityIcon
address={ fromAddress }
button
/>
}
label={
<FormattedMessage
id='deployContract.button.next'
defaultMessage='Next'
/>
}
onClick={ this.onNextStep }
/>
);
const hasParameters = this.state.inputs.length > 0;
const showExtras = this.state.extras;
switch (step) { switch (step) {
case 'CONTRACT_DETAILS': case 'CONTRACT_DETAILS':
return [ return [
cancelBtn, cancelBtn,
<Button hasParameters || showExtras
disabled={ !isValid } ? nextButton
key='next' : createButton
icon={
<IdentityIcon
address={ fromAddress }
button
/>
}
label={
<FormattedMessage
id='deployContract.button.next'
defaultMessage='Next'
/>
}
onClick={ this.onParametersStep }
/>
]; ];
case 'CONTRACT_PARAMETERS': case 'CONTRACT_PARAMETERS':
return [ return [
cancelBtn, cancelBtn,
<Button showExtras
icon={ ? nextButton
<IdentityIcon : createButton
address={ fromAddress } ];
button
/> case 'EXTRAS':
} return [
key='create' cancelBtn,
label={ createButton
<FormattedMessage
id='deployContract.button.create'
defaultMessage='Create'
/>
}
onClick={ this.onDeployStart }
/>
]; ];
case 'DEPLOYMENT': case 'DEPLOYMENT':
@ -344,6 +391,8 @@ class DeployContract extends Component {
{ ...this.state } { ...this.state }
accounts={ accounts } accounts={ accounts }
balances={ balances } balances={ balances }
onAmountChange={ this.onAmountChange }
onExtrasChange={ this.onExtrasChange }
onFromAddressChange={ this.onFromAddressChange } onFromAddressChange={ this.onFromAddressChange }
onDescriptionChange={ this.onDescriptionChange } onDescriptionChange={ this.onDescriptionChange }
onNameChange={ this.onNameChange } onNameChange={ this.onNameChange }
@ -365,6 +414,9 @@ class DeployContract extends Component {
/> />
); );
case 'EXTRAS':
return this.renderExtrasPage();
case 'DEPLOYMENT': case 'DEPLOYMENT':
const body = txhash const body = txhash
? <TxHash hash={ txhash } /> ? <TxHash hash={ txhash } />
@ -411,17 +463,32 @@ class DeployContract extends Component {
} }
} }
renderExtrasPage () {
if (!this.gasStore.histogram) {
return null;
}
return (
<Extras
gasStore={ this.gasStore }
hideData
isEth
/>
);
}
estimateGas = () => { estimateGas = () => {
const { api } = this.context; const { api } = this.context;
const { abiError, abiParsed, code, codeError, fromAddress, fromAddressError, params } = this.state; const { abiError, abiParsed, amountValue, amountError, code, codeError, fromAddress, fromAddressError, params } = this.state;
if (abiError || codeError || fromAddressError) { if (abiError || codeError || fromAddressError || amountError) {
return; return;
} }
const options = { const options = {
data: code, data: code,
from: fromAddress from: fromAddress,
value: amountValue
}; };
const contract = api.newContract(abiParsed); const contract = api.newContract(abiParsed);
@ -437,6 +504,19 @@ class DeployContract extends Component {
}); });
} }
onNextStep = () => {
switch (this.state.step) {
case 'CONTRACT_DETAILS':
return this.onParametersStep();
case 'CONTRACT_PARAMETERS':
return this.onExtrasStep();
default:
console.warn('wrong call of "onNextStep" from', this.state.step);
}
}
onParametersStep = () => { onParametersStep = () => {
const { inputs } = this.state; const { inputs } = this.state;
@ -444,6 +524,14 @@ class DeployContract extends Component {
return this.setState({ step: 'CONTRACT_PARAMETERS' }); return this.setState({ step: 'CONTRACT_PARAMETERS' });
} }
return this.onExtrasStep();
}
onExtrasStep = () => {
if (this.state.extras) {
return this.setState({ step: 'EXTRAS' });
}
return this.onDeployStart(); return this.onDeployStart();
} }
@ -488,10 +576,24 @@ class DeployContract extends Component {
this.setState(validateCode(code), this.estimateGas); this.setState(validateCode(code), this.estimateGas);
} }
onAmountChange = (amount) => {
const { numberError } = validatePositiveNumber(amount);
const nextAmountValue = numberError
? new BigNumber(0)
: this.context.api.util.toWei(amount);
this.gasStore.setEthValue(nextAmountValue);
this.setState({ amount, amountValue: nextAmountValue, amountError: numberError }, this.estimateGas);
}
onExtrasChange = (extras) => {
this.setState({ extras });
}
onDeployStart = () => { onDeployStart = () => {
const { api, store } = this.context; const { api, store } = this.context;
const { source } = this.props; const { source } = this.props;
const { abiParsed, code, description, name, params, fromAddress } = this.state; const { abiParsed, amountValue, code, description, name, params, fromAddress } = this.state;
const metadata = { const metadata = {
abi: abiParsed, abi: abiParsed,
@ -503,16 +605,17 @@ class DeployContract extends Component {
source source
}; };
const options = { const options = this.gasStore.overrideTransaction({
data: code, data: code,
from: fromAddress from: fromAddress,
}; value: amountValue
});
this.setState({ step: 'DEPLOYMENT' }); this.setState({ step: 'DEPLOYMENT' });
const contract = api.newContract(abiParsed); const contract = api.newContract(abiParsed);
deploy(contract, options, params, metadata, this.onDeploymentState) deploy(contract, options, params, metadata, this.onDeploymentState, true)
.then((address) => { .then((address) => {
// No contract address given, might need some confirmations // No contract address given, might need some confirmations
// from the wallet owners... // from the wallet owners...

View File

@ -24,7 +24,7 @@ import { nullableProptype } from '~/util/proptypes';
import TokenSelect from './tokenSelect'; import TokenSelect from './tokenSelect';
import styles from '../transfer.css'; import styles from '../transfer.css';
const CHECK_STYLE = { export const CHECK_STYLE = {
position: 'absolute', position: 'absolute',
top: '38px', top: '38px',
left: '1em' left: '1em'

View File

@ -25,12 +25,17 @@ export default class Extras extends Component {
static propTypes = { static propTypes = {
data: PropTypes.string, data: PropTypes.string,
dataError: PropTypes.string, dataError: PropTypes.string,
hideData: PropTypes.bool,
gasStore: PropTypes.object.isRequired, gasStore: PropTypes.object.isRequired,
isEth: PropTypes.bool, isEth: PropTypes.bool,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func,
total: PropTypes.string, total: PropTypes.string,
totalError: PropTypes.string totalError: PropTypes.string
} };
static defaultProps = {
hideData: false
};
render () { render () {
const { gasStore, onChange } = this.props; const { gasStore, onChange } = this.props;
@ -49,9 +54,9 @@ export default class Extras extends Component {
} }
renderData () { renderData () {
const { isEth, data, dataError } = this.props; const { isEth, data, dataError, hideData } = this.props;
if (!isEth) { if (!isEth || hideData) {
return null; return null;
} }

View File

@ -81,6 +81,7 @@ export default class Input extends Component {
tabIndex: PropTypes.number, tabIndex: PropTypes.number,
type: PropTypes.string, type: PropTypes.string,
submitOnBlur: PropTypes.bool, submitOnBlur: PropTypes.bool,
step: PropTypes.number,
style: PropTypes.object, style: PropTypes.object,
value: PropTypes.oneOfType([ value: PropTypes.oneOfType([
PropTypes.number, PropTypes.number,
@ -124,7 +125,7 @@ export default class Input extends Component {
render () { render () {
const { value } = this.state; const { value } = this.state;
const { autoFocus, children, className, hideUnderline, disabled, error, focused, label } = this.props; const { autoFocus, children, className, hideUnderline, disabled, error, focused, label } = this.props;
const { hint, onClick, multiLine, rows, type, min, max, style, tabIndex } = this.props; const { hint, onClick, multiLine, rows, type, min, max, step, style, tabIndex } = this.props;
const readOnly = this.props.readOnly || disabled; const readOnly = this.props.readOnly || disabled;
@ -179,6 +180,7 @@ export default class Input extends Component {
readOnly={ readOnly } readOnly={ readOnly }
ref='input' ref='input'
rows={ rows } rows={ rows }
step={ step }
style={ textFieldStyle } style={ textFieldStyle }
tabIndex={ tabIndex } tabIndex={ tabIndex }
type={ type || 'text' } type={ type || 'text' }

View File

@ -107,7 +107,7 @@ class MethodDecoding extends Component {
renderGas () { renderGas () {
const { historic, transaction } = this.props; const { historic, transaction } = this.props;
const { gas, gasPrice } = transaction; const { gas, gasPrice, value } = transaction;
if (!gas || !gasPrice) { if (!gas || !gasPrice) {
return null; return null;
@ -126,9 +126,9 @@ class MethodDecoding extends Component {
/> />
</span> </span>
); );
const gasProvidedEth = ( const totalEthValue = (
<span className={ styles.highlight }> <span className={ styles.highlight }>
{ this.renderEtherValue(gas.mul(gasPrice)) } { this.renderEtherValue(gas.mul(gasPrice).plus(value || 0)) }
</span> </span>
); );
const gasUsed = transaction.gasUsed const gasUsed = transaction.gasUsed
@ -149,12 +149,12 @@ class MethodDecoding extends Component {
<div className={ styles.gasDetails }> <div className={ styles.gasDetails }>
<FormattedMessage <FormattedMessage
id='ui.methodDecoding.txValues' id='ui.methodDecoding.txValues'
defaultMessage='{historic, select, true {Provided} false {Provides}} {gasProvided}{gasUsed} for a total transaction value of {gasProvidedEth}' defaultMessage='{historic, select, true {Provided} false {Provides}} {gasProvided}{gasUsed} for a total transaction value of {totalEthValue}'
values={ { values={ {
historic, historic,
gasProvided, gasProvided,
gasProvidedEth, gasUsed,
gasUsed totalEthValue
} } } }
/> />
{ this.renderMinBlock() } { this.renderMinBlock() }
@ -349,6 +349,7 @@ class MethodDecoding extends Component {
renderDeploy () { renderDeploy () {
const { historic, transaction } = this.props; const { historic, transaction } = this.props;
const { methodInputs } = this.state; const { methodInputs } = this.state;
const { value } = transaction;
if (!historic) { if (!historic) {
return ( return (
@ -357,6 +358,19 @@ class MethodDecoding extends Component {
id='ui.methodDecoding.deploy.willDeploy' id='ui.methodDecoding.deploy.willDeploy'
defaultMessage='Will deploy a contract' defaultMessage='Will deploy a contract'
/> />
{
value && value.gt(0)
? (
<FormattedMessage
id='ui.methodDecoding.deploy.withValue'
defaultMessage=', sending {value}'
values={ {
value: this.renderEtherValue(value)
} }
/>
)
: null
}
</div> </div>
); );
} }

View File

@ -73,7 +73,7 @@ export function postTransaction (_func, _options, _values = []) {
}); });
} }
export function deploy (contract, _options, values, metadata = {}, statecb = () => {}) { export function deploy (contract, _options, values, metadata = {}, statecb = () => {}, skipGasEstimate = false) {
const options = { ..._options }; const options = { ..._options };
const { api } = contract; const { api } = contract;
const address = options.from; const address = options.from;
@ -82,16 +82,27 @@ export function deploy (contract, _options, values, metadata = {}, statecb = ()
.isWallet(api, address) .isWallet(api, address)
.then((isWallet) => { .then((isWallet) => {
if (!isWallet) { if (!isWallet) {
return contract.deploy(options, values, statecb); return contract.deploy(options, values, statecb, skipGasEstimate);
} }
statecb(null, { state: 'estimateGas' }); let gasEstPromise;
return deployEstimateGas(contract, options, values) if (skipGasEstimate) {
.then(([gasEst, gas]) => { gasEstPromise = Promise.resolve(null);
options.gas = gas.toFixed(0); } else {
statecb(null, { state: 'estimateGas' });
statecb(null, { state: 'postTransaction', gas }); gasEstPromise = deployEstimateGas(contract, options, values)
.then(([gasEst, gas]) => gas);
}
return gasEstPromise
.then((gas) => {
if (gas) {
options.gas = gas.toFixed(0);
}
statecb(null, { state: 'postTransaction', gas: options.gas });
return WalletsUtils.getDeployArgs(contract, options, values); return WalletsUtils.getDeployArgs(contract, options, values);
}) })

View File

@ -105,7 +105,7 @@ class WriteContract extends Component {
className={ styles.editor } className={ styles.editor }
style={ { flex: `${size}%` } } style={ { flex: `${size}%` } }
> >
<h2>asd{ this.renderTitle() }</h2> <h2>{ this.renderTitle() }</h2>
<Editor <Editor
ref='editor' ref='editor'