Show contract parameters in MethodDecoding (#4024)

* Add decoding of Inner Contract Deployment params #3715

* Fixed TypedInput when formatted value

* Fix TypedInput

* PR Grumble

* Add test to `Param.toParams`
This commit is contained in:
Nicolas Gotchac 2017-01-05 12:06:58 +01:00 committed by Jaco Greeff
parent ddeb06d9cc
commit d16ab5eac5
9 changed files with 207 additions and 64 deletions

View File

@ -31,6 +31,12 @@ export default class Param {
} }
static toParams (params) { static toParams (params) {
return params.map((param) => new Param(param.name, param.type)); return params.map((param) => {
if (param instanceof Param) {
return param;
}
return new Param(param.name, param.type);
});
} }
} }

View File

@ -34,5 +34,14 @@ describe('abi/spec/Param', () => {
expect(params[0].name).to.equal('foo'); expect(params[0].name).to.equal('foo');
expect(params[0].kind.type).to.equal('uint'); expect(params[0].kind.type).to.equal('uint');
}); });
it('converts only if needed', () => {
const _params = Param.toParams([{ name: 'foo', type: 'uint' }]);
const params = Param.toParams(_params);
expect(params.length).to.equal(1);
expect(params[0].name).to.equal('foo');
expect(params[0].kind.type).to.equal('uint');
});
}); });
}); });

View File

@ -26,7 +26,9 @@ export function decodeCallData (data) {
if (data.substr(0, 2) === '0x') { if (data.substr(0, 2) === '0x') {
return decodeCallData(data.slice(2)); return decodeCallData(data.slice(2));
} else if (data.length < 8) { }
if (data.length < 8) {
throw new Error('Input to decodeCallData should be method signature + data'); throw new Error('Input to decodeCallData should be method signature + data');
} }
@ -42,10 +44,14 @@ export function decodeCallData (data) {
export function decodeMethodInput (methodAbi, paramdata) { export function decodeMethodInput (methodAbi, paramdata) {
if (!methodAbi) { if (!methodAbi) {
throw new Error('decodeMethodInput should receive valid method-specific ABI'); throw new Error('decodeMethodInput should receive valid method-specific ABI');
} else if (paramdata && paramdata.length) { }
if (paramdata && paramdata.length) {
if (!isHex(paramdata)) { if (!isHex(paramdata)) {
throw new Error('Input to decodeMethodInput should be a hex value'); throw new Error('Input to decodeMethodInput should be a hex value');
} else if (paramdata.substr(0, 2) === '0x') { }
if (paramdata.substr(0, 2) === '0x') {
return decodeMethodInput(methodAbi, paramdata.slice(2)); return decodeMethodInput(methodAbi, paramdata.slice(2));
} }
} }

View File

@ -60,6 +60,7 @@ class AddressSelect extends Component {
// Optional props // Optional props
allowCopy: PropTypes.bool, allowCopy: PropTypes.bool,
allowInput: PropTypes.bool, allowInput: PropTypes.bool,
className: PropTypes.string,
disabled: PropTypes.bool, disabled: PropTypes.bool,
error: nodeOrStringProptype(), error: nodeOrStringProptype(),
hint: nodeOrStringProptype(), hint: nodeOrStringProptype(),
@ -123,13 +124,14 @@ class AddressSelect extends Component {
renderInput () { renderInput () {
const { focused } = this.state; const { focused } = this.state;
const { accountsInfo, allowCopy, disabled, error, hint, label, readOnly, value } = this.props; const { accountsInfo, allowCopy, className, disabled, error, hint, label, readOnly, value } = this.props;
const input = ( const input = (
<InputAddress <InputAddress
accountsInfo={ accountsInfo } accountsInfo={ accountsInfo }
allowCopy={ allowCopy } allowCopy={ allowCopy }
disabled={ disabled } className={ className }
disabled={ disabled || readOnly }
error={ error } error={ error }
hint={ hint } hint={ hint }
focused={ focused } focused={ focused }

View File

@ -75,6 +75,12 @@ class InputAddress extends Component {
containerClasses.push(styles.small); containerClasses.push(styles.small);
} }
const props = {};
if (!readOnly && !disabled) {
props.focused = focused;
}
return ( return (
<div className={ containerClasses.join(' ') }> <div className={ containerClasses.join(' ') }>
<Input <Input
@ -82,7 +88,6 @@ class InputAddress extends Component {
className={ classes.join(' ') } className={ classes.join(' ') }
disabled={ disabled } disabled={ disabled }
error={ error } error={ error }
focused={ focused }
hideUnderline={ hideUnderline } hideUnderline={ hideUnderline }
hint={ hint } hint={ hint }
label={ label } label={ label }
@ -96,7 +101,9 @@ class InputAddress extends Component {
text && account text && account
? account.name ? account.name
: (nullName || value) : (nullName || value)
} /> }
{ ...props }
/>
{ icon } { icon }
</div> </div>
); );

View File

@ -27,6 +27,7 @@ class InputAddressSelect extends Component {
contracts: PropTypes.object.isRequired, contracts: PropTypes.object.isRequired,
allowCopy: PropTypes.bool, allowCopy: PropTypes.bool,
className: PropTypes.string,
error: PropTypes.string, error: PropTypes.string,
hint: PropTypes.string, hint: PropTypes.string,
label: PropTypes.string, label: PropTypes.string,
@ -36,13 +37,14 @@ class InputAddressSelect extends Component {
}; };
render () { render () {
const { accounts, allowCopy, contacts, contracts, label, hint, error, value, onChange, readOnly } = this.props; const { accounts, allowCopy, className, contacts, contracts, label, hint, error, value, onChange, readOnly } = this.props;
return ( return (
<AddressSelect <AddressSelect
allowCopy={ allowCopy } allowCopy={ allowCopy }
allowInput allowInput
accounts={ accounts } accounts={ accounts }
className={ className }
contacts={ contacts } contacts={ contacts }
contracts={ contracts } contracts={ contracts }
error={ error } error={ error }

View File

@ -41,6 +41,7 @@ export default class TypedInput extends Component {
accounts: PropTypes.object, accounts: PropTypes.object,
allowCopy: PropTypes.bool, allowCopy: PropTypes.bool,
className: PropTypes.string,
error: PropTypes.any, error: PropTypes.any,
hint: PropTypes.string, hint: PropTypes.string,
isEth: PropTypes.bool, isEth: PropTypes.bool,
@ -91,7 +92,7 @@ export default class TypedInput extends Component {
const { type } = param; const { type } = param;
if (type === ABI_TYPES.ARRAY) { if (type === ABI_TYPES.ARRAY) {
const { accounts, label, value = param.default } = this.props; const { accounts, className, label, value = param.default } = this.props;
const { subtype, length } = param; const { subtype, length } = param;
const fixedLength = !!length; const fixedLength = !!length;
@ -107,6 +108,7 @@ export default class TypedInput extends Component {
<TypedInput <TypedInput
accounts={ accounts } accounts={ accounts }
allowCopy={ allowCopy } allowCopy={ allowCopy }
className={ className }
key={ `${subtype.type}_${index}` } key={ `${subtype.type}_${index}` }
onChange={ onChange } onChange={ onChange }
param={ subtype } param={ subtype }
@ -236,17 +238,34 @@ export default class TypedInput extends Component {
); );
} }
getNumberValue (value) {
if (!value) {
return value;
}
const { readOnly } = this.props;
const rawValue = typeof value === 'string'
? value.replace(/,/g, '')
: value;
const bnValue = new BigNumber(rawValue);
return readOnly
? bnValue.toFormat()
: bnValue.toNumber();
}
renderInteger (value = this.props.value, onChange = this.onChange) { renderInteger (value = this.props.value, onChange = this.onChange) {
const { allowCopy, label, error, hint, min, max, readOnly } = this.props; const { allowCopy, className, label, error, hint, min, max, readOnly } = this.props;
const param = this.getParam(); const param = this.getParam();
const realValue = value const realValue = this.getNumberValue(value);
? (new BigNumber(value))[readOnly ? 'toFormat' : 'toNumber']()
: value;
return ( return (
<Input <Input
allowCopy={ allowCopy } allowCopy={ allowCopy }
className={ className }
label={ label } label={ label }
hint={ hint } hint={ hint }
value={ realValue } value={ realValue }
@ -269,16 +288,15 @@ export default class TypedInput extends Component {
* @see https://github.com/facebook/react/issues/1549 * @see https://github.com/facebook/react/issues/1549
*/ */
renderFloat (value = this.props.value, onChange = this.onChange) { renderFloat (value = this.props.value, onChange = this.onChange) {
const { allowCopy, label, error, hint, min, max, readOnly } = this.props; const { allowCopy, className, label, error, hint, min, max, readOnly } = this.props;
const param = this.getParam(); const param = this.getParam();
const realValue = value const realValue = this.getNumberValue(value);
? (new BigNumber(value))[readOnly ? 'toFormat' : 'toNumber']()
: value;
return ( return (
<Input <Input
allowCopy={ allowCopy } allowCopy={ allowCopy }
className={ className }
label={ label } label={ label }
hint={ hint } hint={ hint }
value={ realValue } value={ realValue }
@ -293,11 +311,12 @@ export default class TypedInput extends Component {
} }
renderDefault () { renderDefault () {
const { allowCopy, label, value, error, hint, readOnly } = this.props; const { allowCopy, className, label, value, error, hint, readOnly } = this.props;
return ( return (
<Input <Input
allowCopy={ allowCopy } allowCopy={ allowCopy }
className={ className }
label={ label } label={ label }
hint={ hint } hint={ hint }
value={ value } value={ value }
@ -309,12 +328,13 @@ export default class TypedInput extends Component {
} }
renderAddress () { renderAddress () {
const { accounts, allowCopy, label, value, error, hint, readOnly } = this.props; const { accounts, allowCopy, className, label, value, error, hint, readOnly } = this.props;
return ( return (
<InputAddressSelect <InputAddressSelect
allowCopy={ allowCopy } allowCopy={ allowCopy }
accounts={ accounts } accounts={ accounts }
className={ className }
error={ error } error={ error }
hint={ hint } hint={ hint }
label={ label } label={ label }
@ -326,7 +346,7 @@ export default class TypedInput extends Component {
} }
renderBoolean () { renderBoolean () {
const { allowCopy, label, value, error, hint, readOnly } = this.props; const { allowCopy, className, label, value, error, hint, readOnly } = this.props;
if (readOnly) { if (readOnly) {
return this.renderDefault(); return this.renderDefault();
@ -346,6 +366,7 @@ export default class TypedInput extends Component {
return ( return (
<Select <Select
allowCopy={ allowCopy } allowCopy={ allowCopy }
className={ className }
error={ error } error={ error }
hint={ hint } hint={ hint }
label={ label } label={ label }

View File

@ -14,12 +14,11 @@
// 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 React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import CircularProgress from 'material-ui/CircularProgress'; import CircularProgress from 'material-ui/CircularProgress';
import { Input, InputAddress } from '../Form'; import { TypedInput, InputAddress } from '../Form';
import MethodDecodingStore from './methodDecodingStore'; import MethodDecodingStore from './methodDecodingStore';
import styles from './methodDecoding.css'; import styles from './methodDecoding.css';
@ -245,6 +244,7 @@ class MethodDecoding extends Component {
renderDeploy () { renderDeploy () {
const { historic, transaction } = this.props; const { historic, transaction } = this.props;
const { methodInputs } = this.state;
if (!historic) { if (!historic) {
return ( return (
@ -261,6 +261,14 @@ class MethodDecoding extends Component {
</div> </div>
{ this.renderAddressName(transaction.creates, false) } { this.renderAddressName(transaction.creates, false) }
<div>
{ methodInputs && methodInputs.length ? 'with the following parameters:' : ''}
</div>
<div className={ styles.inputs }>
{ this.renderInputs() }
</div>
</div> </div>
); );
} }
@ -364,39 +372,31 @@ class MethodDecoding extends Component {
renderInputs () { renderInputs () {
const { methodInputs } = this.state; const { methodInputs } = this.state;
return methodInputs.map((input, index) => { if (!methodInputs || methodInputs.length === 0) {
switch (input.type) { return null;
case 'address':
return (
<InputAddress
disabled
text
key={ index }
className={ styles.input }
value={ input.value }
label={ input.type } />
);
default:
return (
<Input
readOnly
allowCopy
key={ index }
className={ styles.input }
value={ this.renderValue(input.value) }
label={ input.type } />
);
} }
const inputs = methodInputs.map((input, index) => {
return (
<TypedInput
allowCopy
className={ styles.input }
label={ input.type }
key={ index }
param={ input.type }
readOnly
value={ this.renderValue(input.value) }
/>
);
}); });
return inputs;
} }
renderValue (value) { renderValue (value) {
const { api } = this.context; const { api } = this.context;
if (api.util.isInstanceOf(value, BigNumber)) { if (api.util.isArray(value)) {
return value.toFormat(0);
} else if (api.util.isArray(value)) {
return api.util.bytesToHex(value); return api.util.bytesToHex(value);
} }

View File

@ -18,6 +18,8 @@ import Contracts from '~/contracts';
import Abi from '~/abi'; import Abi from '~/abi';
import * as abis from '~/contracts/abi'; import * as abis from '~/contracts/abi';
import { decodeMethodInput } from '~/api/util/decode';
const CONTRACT_CREATE = '0x60606040'; const CONTRACT_CREATE = '0x60606040';
let instance = null; let instance = null;
@ -26,6 +28,8 @@ export default class MethodDecodingStore {
api = null; api = null;
_bytecodes = {};
_contractsAbi = {};
_isContract = {}; _isContract = {};
_methods = {}; _methods = {};
@ -46,12 +50,17 @@ export default class MethodDecodingStore {
if (!contract || !contract.meta || !contract.meta.abi) { if (!contract || !contract.meta || !contract.meta.abi) {
return; return;
} }
this.loadFromAbi(contract.meta.abi); this.loadFromAbi(contract.meta.abi, contract.address);
}); });
} }
loadFromAbi (_abi) { loadFromAbi (_abi, contractAddress) {
const abi = new Abi(_abi); const abi = new Abi(_abi);
if (contractAddress && abi) {
this._contractsAbi[contractAddress] = abi;
}
abi abi
.functions .functions
.map((f) => ({ sign: f.signature, abi: f.abi })) .map((f) => ({ sign: f.signature, abi: f.abi }))
@ -111,6 +120,7 @@ export default class MethodDecodingStore {
const contractAddress = isReceived ? transaction.from : transaction.to; const contractAddress = isReceived ? transaction.from : transaction.to;
const input = transaction.input || transaction.data; const input = transaction.input || transaction.data;
result.input = input;
result.received = isReceived; result.received = isReceived;
// No input, should be a ETH transfer // No input, should be a ETH transfer
@ -118,17 +128,20 @@ export default class MethodDecodingStore {
return Promise.resolve(result); return Promise.resolve(result);
} }
try { let signature;
const { signature } = this.api.util.decodeCallData(input);
if (signature === CONTRACT_CREATE || transaction.creates) { try {
result.contract = true; const decodeCallDataResult = this.api.util.decodeCallData(input);
return Promise.resolve({ ...result, deploy: true }); signature = decodeCallDataResult.signature;
}
} catch (e) {} } catch (e) {}
// Contract deployment
if (!signature || signature === CONTRACT_CREATE || transaction.creates) {
return this.decodeContractCreation(result, contractAddress || transaction.creates);
}
return this return this
.isContract(contractAddress || transaction.creates) .isContract(contractAddress)
.then((isContract) => { .then((isContract) => {
result.contract = isContract; result.contract = isContract;
@ -140,11 +153,6 @@ export default class MethodDecodingStore {
result.signature = signature; result.signature = signature;
result.params = paramdata; result.params = paramdata;
// Contract deployment
if (!signature) {
return Promise.resolve({ ...result, deploy: true });
}
return this return this
.fetchMethodAbi(signature) .fetchMethodAbi(signature)
.then((abi) => { .then((abi) => {
@ -173,6 +181,68 @@ export default class MethodDecodingStore {
}); });
} }
decodeContractCreation (data, contractAddress) {
const result = {
...data,
contract: true,
deploy: true
};
const { input } = data;
const abi = this._contractsAbi[contractAddress];
if (!input || !abi || !abi.constructors || abi.constructors.length === 0) {
return Promise.resolve(result);
}
const constructorAbi = abi.constructors[0];
const rawInput = /^(?:0x)?(.*)$/.exec(input)[1];
return this
.getCode(contractAddress)
.then((code) => {
if (!code || /^(0x)0*?$/.test(code)) {
return result;
}
const rawCode = /^(?:0x)?(.*)$/.exec(code)[1];
const codeOffset = rawInput.indexOf(rawCode);
if (codeOffset === -1) {
return result;
}
// Params are the last bytes of the transaction Input
// (minus the bytecode). It seems that they are repeated
// twice
const params = rawInput.slice(codeOffset + rawCode.length);
const paramsBis = params.slice(params.length / 2);
let decodedInputs;
try {
decodedInputs = decodeMethodInput(constructorAbi, params);
} catch (e) {}
try {
if (!decodedInputs) {
decodedInputs = decodeMethodInput(constructorAbi, paramsBis);
}
} catch (e) {}
if (decodedInputs && decodedInputs.length > 0) {
result.inputs = decodedInputs
.map((value, index) => {
const type = constructorAbi.inputs[index].kind.type;
return { type, value };
});
}
return result;
});
}
fetchMethodAbi (signature) { fetchMethodAbi (signature) {
if (this._methods[signature] !== undefined) { if (this._methods[signature] !== undefined) {
return Promise.resolve(this._methods[signature]); return Promise.resolve(this._methods[signature]);
@ -209,7 +279,7 @@ export default class MethodDecodingStore {
return Promise.resolve(this._isContract[contractAddress]); return Promise.resolve(this._isContract[contractAddress]);
} }
this._isContract[contractAddress] = this.api.eth this._isContract[contractAddress] = this
.getCode(contractAddress) .getCode(contractAddress)
.then((bytecode) => { .then((bytecode) => {
// Is a contract if the address contains *valid* bytecode // Is a contract if the address contains *valid* bytecode
@ -222,4 +292,24 @@ export default class MethodDecodingStore {
return Promise.resolve(this._isContract[contractAddress]); return Promise.resolve(this._isContract[contractAddress]);
} }
getCode (contractAddress) {
// If zero address, resolve to '0x'
if (!contractAddress || /^(0x)?0*$/.test(contractAddress)) {
return Promise.resolve('0x');
}
if (this._bytecodes[contractAddress]) {
return Promise.resolve(this._bytecodes[contractAddress]);
}
this._bytecodes[contractAddress] = this.api.eth
.getCode(contractAddress)
.then((bytecode) => {
this._bytecodes[contractAddress] = bytecode;
return this._bytecodes[contractAddress];
});
return Promise.resolve(this._bytecodes[contractAddress]);
}
} }