Merge pull request #3430 from ethcore/ng-array-parameters

Handle contract constructor inputs
This commit is contained in:
Jaco Greeff 2016-11-15 14:35:10 +01:00 committed by GitHub
commit 28f11be200
12 changed files with 497 additions and 96 deletions

View File

@ -46,7 +46,8 @@ function stringToBytes (input) {
if (isArray(input)) { if (isArray(input)) {
return input; return input;
} else if (input.substr(0, 2) === '0x') { } else if (input.substr(0, 2) === '0x') {
return input.substr(2).toLowerCase().match(/.{1,2}/g).map((value) => parseInt(value, 16)); const matches = input.substr(2).toLowerCase().match(/.{1,2}/g) || [];
return matches.map((value) => parseInt(value, 16));
} else { } else {
return input.split('').map((char) => char.charCodeAt(0)); return input.split('').map((char) => char.charCodeAt(0));
} }

View File

@ -15,10 +15,10 @@
// 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 { MenuItem } from 'material-ui';
import { AddressSelect, Form, Input, InputAddressSelect, Select } from '../../../ui'; import { AddressSelect, Form, Input, TypedInput } from '../../../ui';
import { validateAbi } from '../../../util/validation'; import { validateAbi } from '../../../util/validation';
import { parseAbiType } from '../../../util/abi';
import styles from '../deployContract.css'; import styles from '../deployContract.css';
@ -103,6 +103,7 @@ export default class DetailsStep extends Component {
value={ code } value={ code }
onSubmit={ this.onCodeChange } onSubmit={ this.onCodeChange }
readOnly={ readOnly } /> readOnly={ readOnly } />
{ this.renderConstructorInputs() } { this.renderConstructorInputs() }
</Form> </Form>
); );
@ -117,59 +118,23 @@ export default class DetailsStep extends Component {
} }
return inputs.map((input, index) => { return inputs.map((input, index) => {
const onChange = (event, value) => this.onParamChange(index, value); const onChange = (value) => this.onParamChange(index, value);
const onChangeBool = (event, _index, value) => this.onParamChange(index, value === 'true');
const onSubmit = (value) => this.onParamChange(index, value);
const label = `${input.name}: ${input.type}`;
let inputBox = null;
switch (input.type) { const label = `${input.name ? `${input.name}: ` : ''}${input.type}`;
case 'address': const value = params[index];
inputBox = ( const error = paramsError[index];
<InputAddressSelect const param = parseAbiType(input.type);
accounts={ accounts }
editing
label={ label }
value={ params[index] }
error={ paramsError[index] }
onChange={ onChange } />
);
break;
case 'bool':
const boolitems = ['false', 'true'].map((bool) => {
return (
<MenuItem
key={ bool }
value={ bool }
label={ bool }>{ bool }</MenuItem>
);
});
inputBox = (
<Select
label={ label }
value={ params[index] ? 'true' : 'false' }
error={ paramsError[index] }
onChange={ onChangeBool }>
{ boolitems }
</Select>
);
break;
default:
inputBox = (
<Input
label={ label }
value={ params[index] }
error={ paramsError[index] }
onSubmit={ onSubmit } />
);
break;
}
return ( return (
<div key={ index } className={ styles.funcparams }> <div key={ index } className={ styles.funcparams }>
{ inputBox } <TypedInput
label={ label }
value={ value }
error={ error }
accounts={ accounts }
onChange={ onChange }
param={ param }
/>
</div> </div>
); );
}); });
@ -200,35 +165,14 @@ export default class DetailsStep extends Component {
const { abiError, abiParsed } = validateAbi(abi, api); const { abiError, abiParsed } = validateAbi(abi, api);
if (!abiError) { if (!abiError) {
const { inputs } = abiParsed.find((method) => method.type === 'constructor') || { inputs: [] }; const { inputs } = abiParsed
.find((method) => method.type === 'constructor') || { inputs: [] };
const params = []; const params = [];
inputs.forEach((input) => { inputs.forEach((input) => {
switch (input.type) { const param = parseAbiType(input.type);
case 'address': params.push(param.default);
params.push('0x');
break;
case 'bool':
params.push(false);
break;
case 'bytes':
params.push('0x');
break;
case 'uint':
params.push('0');
break;
case 'string':
params.push('');
break;
default:
params.push('0');
break;
}
}); });
onParamsChange(params); onParamsChange(params);

View File

@ -101,7 +101,8 @@ export default class DeployContract extends Component {
steps={ deployError ? null : steps } steps={ deployError ? null : steps }
title={ deployError ? 'deployment failed' : null } title={ deployError ? 'deployment failed' : null }
waiting={ [1] } waiting={ [1] }
visible> visible
scroll>
{ this.renderStep() } { this.renderStep() }
</Modal> </Modal>
); );
@ -118,8 +119,22 @@ export default class DeployContract extends Component {
onClick={ this.onClose } /> onClick={ this.onClose } />
); );
const closeBtn = (
<Button
icon={ <ContentClear /> }
label='Close'
onClick={ this.onClose } />
);
const closeBtnOk = (
<Button
icon={ <ActionDoneAll /> }
label='Close'
onClick={ this.onClose } />
);
if (deployError) { if (deployError) {
return cancelBtn; return closeBtn;
} }
switch (step) { switch (step) {
@ -134,17 +149,10 @@ export default class DeployContract extends Component {
]; ];
case 1: case 1:
return [ return [ closeBtn ];
cancelBtn
];
case 2: case 2:
return [ return [ closeBtnOk ];
<Button
icon={ <ActionDoneAll /> }
label='Close'
onClick={ this.onClose } />
];
} }
} }
@ -277,8 +285,6 @@ export default class DeployContract extends Component {
return; return;
} }
console.log('onDeploymentState', data);
switch (data.state) { switch (data.state) {
case 'estimateGas': case 'estimateGas':
case 'postTransaction': case 'postTransaction':

View File

@ -39,6 +39,10 @@
position: absolute; position: absolute;
left: 0; left: 0;
top: 35px; top: 35px;
&.noLabel {
top: 11px;
}
} }
.paddedInput input { .paddedInput input {

View File

@ -106,15 +106,21 @@ export default class AddressSelect extends Component {
} }
renderIdentityIcon (inputValue) { renderIdentityIcon (inputValue) {
const { error, value } = this.props; const { error, value, label } = this.props;
if (error || !inputValue || value.length !== 42) { if (error || !inputValue || value.length !== 42) {
return null; return null;
} }
const classes = [ styles.icon ];
if (!label) {
classes.push(styles.noLabel);
}
return ( return (
<IdentityIcon <IdentityIcon
className={ styles.icon } className={ classes.join(' ') }
inline center inline center
address={ value } /> address={ value } />
); );

View File

@ -63,7 +63,9 @@ export default class Input extends Component {
hideUnderline: PropTypes.bool, hideUnderline: PropTypes.bool,
value: PropTypes.oneOfType([ value: PropTypes.oneOfType([
PropTypes.number, PropTypes.string PropTypes.number, PropTypes.string
]) ]),
min: PropTypes.any,
max: PropTypes.any
}; };
static defaultProps = { static defaultProps = {
@ -86,7 +88,7 @@ export default class Input extends Component {
render () { render () {
const { value } = this.state; const { value } = this.state;
const { children, className, hideUnderline, disabled, error, label, hint, multiLine, rows, type } = this.props; const { children, className, hideUnderline, disabled, error, label, hint, multiLine, rows, type, min, max } = this.props;
const readOnly = this.props.readOnly || disabled; const readOnly = this.props.readOnly || disabled;
@ -130,6 +132,8 @@ export default class Input extends Component {
onChange={ this.onChange } onChange={ this.onChange }
onKeyDown={ this.onKeyDown } onKeyDown={ this.onKeyDown }
inputStyle={ inputStyle } inputStyle={ inputStyle }
min={ min }
max={ max }
> >
{ children } { children }
</TextField> </TextField>

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (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 './typedInput';

View File

@ -0,0 +1,31 @@
/* Copyright 2015, 2016 Ethcore (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/>.
*/
.inputs {
padding-top: 2px;
overflow-x: hidden;
label {
line-height: 22px;
pointer-events: none;
color: rgba(255, 255, 255, 0.498039);
-webkit-user-select: none;
font-size: 12px;
top: 11px;
position: relative;
}
}

View File

@ -0,0 +1,239 @@
// Copyright 2015, 2016 Ethcore (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 { MenuItem } from 'material-ui';
import { range } from 'lodash';
import IconButton from 'material-ui/IconButton';
import AddIcon from 'material-ui/svg-icons/content/add';
import RemoveIcon from 'material-ui/svg-icons/content/remove';
import { Input, InputAddressSelect, Select } from '../../../ui';
import { ABI_TYPES } from '../../../util/abi';
import styles from './typedInput.css';
export default class TypedInput extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
accounts: PropTypes.object.isRequired,
param: PropTypes.object.isRequired,
error: PropTypes.any,
value: PropTypes.any,
label: PropTypes.string
};
render () {
const { param } = this.props;
const { type } = param;
if (type === ABI_TYPES.ARRAY) {
const { accounts, label, value = param.default } = this.props;
const { subtype, length } = param;
const fixedLength = !!length;
const inputs = range(length || value.length).map((_, index) => {
const onChange = (inputValue) => {
const newValues = [].concat(this.props.value);
newValues[index] = inputValue;
this.props.onChange(newValues);
};
return (
<TypedInput
key={ `${subtype.type}_${index}` }
onChange={ onChange }
accounts={ accounts }
param={ subtype }
value={ value[index] }
/>
);
});
return (
<div className={ styles.inputs }>
<label>{ label }</label>
{ fixedLength ? null : this.renderLength() }
{ inputs }
</div>
);
}
return this.renderType(type);
}
renderLength () {
const iconStyle = {
width: 16,
height: 16
};
const style = {
width: 32,
height: 32,
padding: 0
};
return (
<div>
<IconButton
iconStyle={ iconStyle }
style={ style }
onClick={ this.onAddField }
>
<AddIcon />
</IconButton>
<IconButton
iconStyle={ iconStyle }
style={ style }
onClick={ this.onRemoveField }
>
<RemoveIcon />
</IconButton>
</div>
);
}
renderType (type) {
if (type === ABI_TYPES.ADDRESS) {
return this.renderAddress();
}
if (type === ABI_TYPES.BOOL) {
return this.renderBoolean();
}
if (type === ABI_TYPES.STRING) {
return this.renderDefault();
}
if (type === ABI_TYPES.BYTES) {
return this.renderDefault();
}
if (type === ABI_TYPES.INT) {
return this.renderNumber();
}
if (type === ABI_TYPES.FIXED) {
return this.renderNumber();
}
return this.renderDefault();
}
renderNumber () {
const { label, value, error, param } = this.props;
return (
<Input
label={ label }
value={ value }
error={ error }
onSubmit={ this.onSubmit }
type='number'
min={ param.signed ? null : 0 }
/>
);
}
renderDefault () {
const { label, value, error } = this.props;
return (
<Input
label={ label }
value={ value }
error={ error }
onSubmit={ this.onSubmit }
/>
);
}
renderAddress () {
const { accounts, label, value, error } = this.props;
return (
<InputAddressSelect
accounts={ accounts }
label={ label }
value={ value }
error={ error }
onChange={ this.onChange }
editing
/>
);
}
renderBoolean () {
const { label, value, error } = this.props;
const boolitems = ['false', 'true'].map((bool) => {
return (
<MenuItem
key={ bool }
value={ bool }
label={ bool }
>
{ bool }
</MenuItem>
);
});
return (
<Select
label={ label }
value={ value ? 'true' : 'false' }
error={ error }
onChange={ this.onChangeBool }
>
{ boolitems }
</Select>
);
}
onChangeBool = (event, _index, value) => {
this.props.onChange(value === 'true');
}
onChange = (event, value) => {
this.props.onChange(value);
}
onSubmit = (value) => {
this.props.onChange(value);
}
onAddField = () => {
const { value, onChange, param } = this.props;
const newValues = [].concat(value, param.subtype.default);
onChange(newValues);
}
onRemoveField = () => {
const { value, onChange } = this.props;
const newValues = value.slice(0, -1);
onChange(newValues);
}
}

View File

@ -16,6 +16,7 @@
import AddressSelect from './AddressSelect'; import AddressSelect from './AddressSelect';
import FormWrap from './FormWrap'; import FormWrap from './FormWrap';
import TypedInput from './TypedInput';
import Input from './Input'; import Input from './Input';
import InputAddress from './InputAddress'; import InputAddress from './InputAddress';
import InputAddressSelect from './InputAddressSelect'; import InputAddressSelect from './InputAddressSelect';
@ -27,6 +28,7 @@ export default from './form';
export { export {
AddressSelect, AddressSelect,
FormWrap, FormWrap,
TypedInput,
Input, Input,
InputAddress, InputAddress,
InputAddressSelect, InputAddressSelect,

View File

@ -29,7 +29,7 @@ import ContextProvider from './ContextProvider';
import CopyToClipboard from './CopyToClipboard'; import CopyToClipboard from './CopyToClipboard';
import Editor from './Editor'; import Editor from './Editor';
import Errors from './Errors'; import Errors from './Errors';
import Form, { AddressSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select } from './Form'; import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select } from './Form';
import IdentityIcon from './IdentityIcon'; import IdentityIcon from './IdentityIcon';
import IdentityName from './IdentityName'; import IdentityName from './IdentityName';
import MethodDecoding from './MethodDecoding'; import MethodDecoding from './MethodDecoding';
@ -62,6 +62,7 @@ export {
Errors, Errors,
Form, Form,
FormWrap, FormWrap,
TypedInput,
Input, Input,
InputAddress, InputAddress,
InputAddressSelect, InputAddressSelect,

146
js/src/util/abi.js Normal file
View File

@ -0,0 +1,146 @@
// Copyright 2015, 2016 Ethcore (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 { range } from 'lodash';
const ARRAY_TYPE = 'ARRAY_TYPE';
const ADDRESS_TYPE = 'ADDRESS_TYPE';
const STRING_TYPE = 'STRING_TYPE';
const BOOL_TYPE = 'BOOL_TYPE';
const BYTES_TYPE = 'BYTES_TYPE';
const INT_TYPE = 'INT_TYPE';
const FIXED_TYPE = 'FIXED_TYPE';
export const ABI_TYPES = {
ARRAY: ARRAY_TYPE, ADDRESS: ADDRESS_TYPE,
STRING: STRING_TYPE, BOOL: BOOL_TYPE,
BYTES: BYTES_TYPE, INT: INT_TYPE,
FIXED: FIXED_TYPE
};
export function parseAbiType (type) {
const arrayRegex = /^(.+)\[(\d*)]$/;
if (arrayRegex.test(type)) {
const matches = arrayRegex.exec(type);
const subtype = parseAbiType(matches[1]);
const M = parseInt(matches[2]) || null;
const defaultValue = !M
? []
: range(M).map(() => subtype.default);
return {
type: ARRAY_TYPE,
subtype: subtype,
length: M,
default: defaultValue
};
}
const lengthRegex = /^(u?int|bytes)(\d{1,3})$/;
if (lengthRegex.test(type)) {
const matches = lengthRegex.exec(type);
const subtype = parseAbiType(matches[1]);
const length = parseInt(matches[2]);
return {
...subtype,
length
};
}
const fixedLengthRegex = /^(u?fixed)(\d{1,3})x(\d{1,3})$/;
if (fixedLengthRegex.test(type)) {
const matches = fixedLengthRegex.exec(type);
const subtype = parseAbiType(matches[1]);
const M = parseInt(matches[2]);
const N = parseInt(matches[3]);
return {
...subtype,
M, N
};
}
if (type === 'string') {
return {
type: STRING_TYPE,
default: ''
};
}
if (type === 'bool') {
return {
type: BOOL_TYPE,
default: false
};
}
if (type === 'address') {
return {
type: ADDRESS_TYPE,
default: ''
};
}
if (type === 'bytes') {
return {
type: BYTES_TYPE,
default: '0x'
};
}
if (type === 'uint') {
return {
type: INT_TYPE,
default: 0,
length: 256,
signed: false
};
}
if (type === 'int') {
return {
type: INT_TYPE,
default: 0,
length: 256,
signed: true
};
}
if (type === 'ufixed') {
return {
type: FIXED_TYPE,
default: 0,
length: 256,
signed: false
};
}
if (type === 'fixed') {
return {
type: FIXED_TYPE,
default: 0,
length: 256,
signed: true
};
}
}