From d16ab5eac50963c0ef9fb5bfa9e33a75f68cf914 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Thu, 5 Jan 2017 12:06:58 +0100 Subject: [PATCH] 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` --- js/src/abi/spec/param.js | 8 +- js/src/abi/spec/param.spec.js | 9 ++ js/src/api/util/decode.js | 12 +- js/src/ui/Form/AddressSelect/addressSelect.js | 6 +- js/src/ui/Form/InputAddress/inputAddress.js | 11 +- .../InputAddressSelect/inputAddressSelect.js | 4 +- js/src/ui/Form/TypedInput/typedInput.js | 45 +++++-- js/src/ui/MethodDecoding/methodDecoding.js | 56 ++++---- .../ui/MethodDecoding/methodDecodingStore.js | 120 +++++++++++++++--- 9 files changed, 207 insertions(+), 64 deletions(-) diff --git a/js/src/abi/spec/param.js b/js/src/abi/spec/param.js index 88696ceed..d7a85c009 100644 --- a/js/src/abi/spec/param.js +++ b/js/src/abi/spec/param.js @@ -31,6 +31,12 @@ export default class Param { } 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); + }); } } diff --git a/js/src/abi/spec/param.spec.js b/js/src/abi/spec/param.spec.js index 9957df909..c1dcddeb5 100644 --- a/js/src/abi/spec/param.spec.js +++ b/js/src/abi/spec/param.spec.js @@ -34,5 +34,14 @@ describe('abi/spec/Param', () => { expect(params[0].name).to.equal('foo'); 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'); + }); }); }); diff --git a/js/src/api/util/decode.js b/js/src/api/util/decode.js index 0e0164bec..d0cea05c1 100644 --- a/js/src/api/util/decode.js +++ b/js/src/api/util/decode.js @@ -26,7 +26,9 @@ export function decodeCallData (data) { if (data.substr(0, 2) === '0x') { 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'); } @@ -42,10 +44,14 @@ export function decodeCallData (data) { export function decodeMethodInput (methodAbi, paramdata) { if (!methodAbi) { throw new Error('decodeMethodInput should receive valid method-specific ABI'); - } else if (paramdata && paramdata.length) { + } + + if (paramdata && paramdata.length) { if (!isHex(paramdata)) { 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)); } } diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index ba48b6489..692ff4285 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -60,6 +60,7 @@ class AddressSelect extends Component { // Optional props allowCopy: PropTypes.bool, allowInput: PropTypes.bool, + className: PropTypes.string, disabled: PropTypes.bool, error: nodeOrStringProptype(), hint: nodeOrStringProptype(), @@ -123,13 +124,14 @@ class AddressSelect extends Component { renderInput () { 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 = ( + } + { ...props } + /> { icon } ); diff --git a/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js b/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js index 60a0f8d1b..f5b218694 100644 --- a/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js +++ b/js/src/ui/Form/InputAddressSelect/inputAddressSelect.js @@ -27,6 +27,7 @@ class InputAddressSelect extends Component { contracts: PropTypes.object.isRequired, allowCopy: PropTypes.bool, + className: PropTypes.string, error: PropTypes.string, hint: PropTypes.string, label: PropTypes.string, @@ -36,13 +37,14 @@ class InputAddressSelect extends Component { }; 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 ( . -import BigNumber from 'bignumber.js'; import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import CircularProgress from 'material-ui/CircularProgress'; -import { Input, InputAddress } from '../Form'; +import { TypedInput, InputAddress } from '../Form'; import MethodDecodingStore from './methodDecodingStore'; import styles from './methodDecoding.css'; @@ -245,6 +244,7 @@ class MethodDecoding extends Component { renderDeploy () { const { historic, transaction } = this.props; + const { methodInputs } = this.state; if (!historic) { return ( @@ -261,6 +261,14 @@ class MethodDecoding extends Component { { this.renderAddressName(transaction.creates, false) } + +
+ { methodInputs && methodInputs.length ? 'with the following parameters:' : ''} +
+ +
+ { this.renderInputs() } +
); } @@ -364,39 +372,31 @@ class MethodDecoding extends Component { renderInputs () { const { methodInputs } = this.state; - return methodInputs.map((input, index) => { - switch (input.type) { - case 'address': - return ( - - ); + if (!methodInputs || methodInputs.length === 0) { + return null; + } - default: - return ( - - ); - } + const inputs = methodInputs.map((input, index) => { + return ( + + ); }); + + return inputs; } renderValue (value) { const { api } = this.context; - if (api.util.isInstanceOf(value, BigNumber)) { - return value.toFormat(0); - } else if (api.util.isArray(value)) { + if (api.util.isArray(value)) { return api.util.bytesToHex(value); } diff --git a/js/src/ui/MethodDecoding/methodDecodingStore.js b/js/src/ui/MethodDecoding/methodDecodingStore.js index 5d518d3a9..b31412c21 100644 --- a/js/src/ui/MethodDecoding/methodDecodingStore.js +++ b/js/src/ui/MethodDecoding/methodDecodingStore.js @@ -18,6 +18,8 @@ import Contracts from '~/contracts'; import Abi from '~/abi'; import * as abis from '~/contracts/abi'; +import { decodeMethodInput } from '~/api/util/decode'; + const CONTRACT_CREATE = '0x60606040'; let instance = null; @@ -26,6 +28,8 @@ export default class MethodDecodingStore { api = null; + _bytecodes = {}; + _contractsAbi = {}; _isContract = {}; _methods = {}; @@ -46,12 +50,17 @@ export default class MethodDecodingStore { if (!contract || !contract.meta || !contract.meta.abi) { return; } - this.loadFromAbi(contract.meta.abi); + this.loadFromAbi(contract.meta.abi, contract.address); }); } - loadFromAbi (_abi) { + loadFromAbi (_abi, contractAddress) { const abi = new Abi(_abi); + + if (contractAddress && abi) { + this._contractsAbi[contractAddress] = abi; + } + abi .functions .map((f) => ({ sign: f.signature, abi: f.abi })) @@ -111,6 +120,7 @@ export default class MethodDecodingStore { const contractAddress = isReceived ? transaction.from : transaction.to; const input = transaction.input || transaction.data; + result.input = input; result.received = isReceived; // No input, should be a ETH transfer @@ -118,17 +128,20 @@ export default class MethodDecodingStore { return Promise.resolve(result); } - try { - const { signature } = this.api.util.decodeCallData(input); + let signature; - if (signature === CONTRACT_CREATE || transaction.creates) { - result.contract = true; - return Promise.resolve({ ...result, deploy: true }); - } + try { + const decodeCallDataResult = this.api.util.decodeCallData(input); + signature = decodeCallDataResult.signature; } catch (e) {} + // Contract deployment + if (!signature || signature === CONTRACT_CREATE || transaction.creates) { + return this.decodeContractCreation(result, contractAddress || transaction.creates); + } + return this - .isContract(contractAddress || transaction.creates) + .isContract(contractAddress) .then((isContract) => { result.contract = isContract; @@ -140,11 +153,6 @@ export default class MethodDecodingStore { result.signature = signature; result.params = paramdata; - // Contract deployment - if (!signature) { - return Promise.resolve({ ...result, deploy: true }); - } - return this .fetchMethodAbi(signature) .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) { if (this._methods[signature] !== undefined) { return Promise.resolve(this._methods[signature]); @@ -209,7 +279,7 @@ export default class MethodDecodingStore { return Promise.resolve(this._isContract[contractAddress]); } - this._isContract[contractAddress] = this.api.eth + this._isContract[contractAddress] = this .getCode(contractAddress) .then((bytecode) => { // Is a contract if the address contains *valid* bytecode @@ -222,4 +292,24 @@ export default class MethodDecodingStore { 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]); + } + }